import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.event.MouseEvent; import java.awt.geom.Ellipse2D; import java.awt.geom.Line2D; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.event.MouseInputListener; public class CirclePlane extends JPanel implements MouseInputListener { private static final long serialVersionUID = 1L; public static void main( String[] args ) { JFrame window = new JFrame(); window.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); window.setTitle( "Circle Plane" ); window.setLocationRelativeTo( null ); CirclePlane space = new CirclePlane(); window.add( space ); window.setSize( 640, 480 ); window.setResizable( false ); window.setVisible( true ); space.start(); } public CirclePlane() { setBackground( Color.BLACK ); addMouseListener( this ); addMouseMotionListener( this ); } public static final Font FONT = new Font( "Monospaced", Font.PLAIN, 12 ); private enum DraggingState { START, END, RADIUS, NONE, PLANE_POINT, PLANE_NORMAL; } private class Intersection { public float cx, cy, time, nx, ny, ix, iy; public Intersection( float x, float y, float time, float nx, float ny, float ix, float iy ) { this.cx = x; this.cy = y; this.time = time; this.nx = nx; this.ny = ny; this.ix = ix; this.iy = iy; } } private float pointRadius = 8.0f; private Vector start; private Vector end; private Vector radiusPoint; private float radius; private Vector planePoint; private Vector planeNormal; private DraggingState dragging; public void start() { planePoint = new Vector( 150, 150 ); planeNormal = new Vector( 120, 120 ); start = new Vector( 50, 400 ); end = new Vector( 320, 240 ); radius = 40.0f; radiusPoint = new Vector( start.x, start.y - radius ); dragging = DraggingState.NONE; } public void paint( Graphics g ) { Graphics2D g2d = (Graphics2D)g; g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON ); g2d.setColor( getBackground() ); g2d.fillRect( 0, 0, getWidth(), getHeight() ); Vector normal = planeNormal.sub( planePoint ).normali(); g2d.setColor( Color.BLUE ); g2d.draw( new Line2D.Float( planeNormal.x, planeNormal.y, planePoint.x, planePoint.y ) ); g2d.draw( new Line2D.Float( planePoint.x + normal.y * 300, planePoint.y - normal.x * 300, planePoint.x - normal.y * 300, planePoint.y + normal.x * 300 ) ); g2d.setColor( Color.WHITE ); g2d.draw( new Line2D.Float( start.x, start.y, end.x, end.y ) ); g2d.setColor( Color.GREEN ); g2d.draw( new Ellipse2D.Float( start.x - pointRadius, start.y - pointRadius, pointRadius * 2, pointRadius * 2 ) ); g2d.setColor( Color.RED ); g2d.draw( new Ellipse2D.Float( end.x - pointRadius, end.y - pointRadius, pointRadius * 2, pointRadius * 2 ) ); g2d.setColor( Color.YELLOW ); g2d.draw( new Ellipse2D.Float( radiusPoint.x - pointRadius, radiusPoint.y - pointRadius, pointRadius * 2, pointRadius * 2 ) ); g2d.draw( new Ellipse2D.Float( planePoint.x - pointRadius, planePoint.y - pointRadius, pointRadius * 2, pointRadius * 2 ) ); g2d.draw( new Ellipse2D.Float( planeNormal.x - pointRadius, planeNormal.y - pointRadius, pointRadius * 2, pointRadius * 2 ) ); g2d.draw( new Ellipse2D.Float( start.x - radius, start.y - radius, radius * 2, radius * 2 ) ); g2d.draw( new Ellipse2D.Float( end.x - radius, end.y - radius, radius * 2, radius * 2 ) ); // Check for intersection g2d.setColor( Color.LIGHT_GRAY ); g2d.setFont( FONT ); Intersection inter = handleIntersection( fromPoint( planePoint, normal ), start, end, radius ); if (inter != null) { g2d.setColor( Color.LIGHT_GRAY ); g2d.drawString( "time: " + inter.time, 10, 20 ); g2d.setColor( Color.GRAY ); g2d.draw( new Ellipse2D.Float( inter.cx - radius, inter.cy - radius, radius * 2, radius * 2 ) ); g2d.draw( new Line2D.Float( inter.cx, inter.cy, inter.cx + inter.nx * 20, inter.cy + inter.ny * 20 ) ); g2d.setColor( Color.RED ); g2d.draw( new Ellipse2D.Float( inter.ix - 2, inter.iy - 2, 4, 4 ) ); // Project Future Position float remainingTime = 1.0f - inter.time; float dx = end.x - start.x; float dy = end.y - start.y; float dot = dx * inter.nx + dy * inter.ny; float ndx = dx - 2 * dot * inter.nx; float ndy = dy - 2 * dot * inter.ny; float newx = inter.cx + ndx * remainingTime; float newy = inter.cy + ndy * remainingTime; g2d.setColor( Color.darkGray ); g2d.draw( new Ellipse2D.Float( newx - radius, newy - radius, radius * 2, radius * 2 ) ); g2d.draw( new Line2D.Float( inter.cx, inter.cy, newx, newy ) ); } } private Intersection handleIntersection( Plane plane, Vector start, Vector end, float radius ) { // No intersection if start is already intersecting with the plane or // end is not intersecting with the plane. if (plane.distance( start ) < radius || plane.distance( end ) > radius) { return null; } Plane shifted = new Plane( plane.a, plane.b, plane.c - radius ); Plane line = fromLine( start, end ); Vector intersection = new Vector(); shifted.intersection( line, intersection ); float distance = start.distance( intersection ); float time = distance / start.distance( end ); return new Intersection( intersection.x, intersection.y, time, plane.a, plane.b, intersection.x - (plane.a * radius), intersection.y - (plane.b * radius) ); } public void mousePressed( MouseEvent e ) { Vector mouse = new Vector( e.getX(), e.getY() ); if (mouse.distance( start ) <= pointRadius) { dragging = DraggingState.START; } else if (mouse.distance( end ) <= pointRadius) { dragging = DraggingState.END; } else if (mouse.distance( radiusPoint ) <= pointRadius) { dragging = DraggingState.RADIUS; } else if (mouse.distance( planeNormal ) <= pointRadius) { dragging = DraggingState.PLANE_NORMAL; } else if (mouse.distance( planePoint ) <= pointRadius) { dragging = DraggingState.PLANE_POINT; } else { dragging = DraggingState.NONE; } } public void mouseReleased( MouseEvent e ) { dragging = DraggingState.NONE; } public void mouseDragged( MouseEvent e ) { Vector mouse = new Vector( e.getX(), e.getY() ); switch (dragging) { case END: end.set( mouse ); break; case RADIUS: radiusPoint.set( mouse ); radius = radiusPoint.distance( start ); break; case START: start.set( mouse ); radiusPoint.set( mouse ); radiusPoint.y -= radius; break; case PLANE_NORMAL: planeNormal.set( mouse ); break; case PLANE_POINT: Vector diff = planeNormal.sub( planePoint ); planePoint.set( mouse ); planeNormal.set( mouse ); planeNormal.addi( diff ); break; case NONE: break; } repaint(); } // Unused Mouse Listener Methods public void mouseMoved( MouseEvent e ) { } public void mouseClicked( MouseEvent e ) { } public void mouseEntered( MouseEvent e ) { } public void mouseExited( MouseEvent e ) { } public Plane fromPoint( Vector point, Vector normal ) { return new Plane( normal.x, normal.y, -normal.dot( point ) ); } public Plane fromLine( Vector s, Vector e ) { float dx = (e.x - s.x); float dy = (e.y - s.y); float d = 1.0f / (float)Math.sqrt( dx * dx + dy * dy ); float a = -dy * d; float b = dx * d; float c = -(a * s.x + b * s.y); return new Plane( a, b, c ); } public class Plane { public float a, b, c; public Plane( float a, float b, float c ) { this.a = a; this.b = b; this.c = c; } public float distance( Vector v ) { return distance( v.x, v.y ); } public float distance( float x, float y ) { return (a * x + b * y + c); } public boolean intersection( Plane p, Vector out ) { float div = (a * p.b - b * p.a); if (div == 0) { return false; } div = 1 / div; out.x = (-c * p.b + b * p.c) * div; out.y = (-a * p.c + c * p.a) * div; return true; } } }