/** * File: HW3_Sample.java * Author: Marsette Vona * Created: 2/22/10 **/ import javax.swing.*; import java.awt.*; import java.awt.geom.*; import java.awt.event.*; import java.util.*; /** *

This is an example using the Java2D API for CS4300 Homework 3, a 2D * interactive graphics program.

* *

To compile: javac HW3_Sample.java

* *

To generate doc: javadoc -d doc HW3_Sample.java

* *

To run: java HW3_Sample

* *

Note: this code requires Java 1.6 or greater.

* *

The example implements a {@link World} with a list of a few * simple {@link Geom}s. Each can be interactively picked, translated, and * rotated (hold shift). The canvas can also be panned and zoomed (hold ctrl) * by dragging outside any object.

* *

{@link #main} creates a singleton instance of HW3_Sample. The * constructor then takes over, and crates a frame populated with one {@link * World}.

* *

On some platforms, "tearing" artifacts may be present in the rendering * when things move. This is a known limitation * of Java rendering. Though Swing is double-buffered by default, buffer swaps * are not synchronized to the hardware vertical retrace. It is currently * possible to overcome this only by using a special full-screen window. See * this * post for some more info.

**/ public class HW3_Sample { /** initial window size **/ public static final int WINDOW_SIZE = 512; /** drawing clear color **/ public static final Color BACKGROUND_COLOR = Color.WHITE; /** rotation degrees per dragged pixel **/ public static final float ROT_DEG_PER_PIXEL = 5.0f; /** zoom amount per pixel **/ public static final float ZOOM_PER_PIXEL = 0.01f; /** min and max zoom **/ public static final float MIN_ZOOM = 0.1f, MAX_ZOOM = 4.0f; /** *

This class represents a drawable shape, posed on a {@link World}.

**/ public class Geom { /** the shape to draw **/ protected final Shape shape; /** *

The paint to use to draw {@link #shape}, defaults to color black.

**/ protected final Paint paint; /** whehter to fill the {@link #shape}, default false **/ protected final boolean filled; /** *

The stroke to use to draw {@link #shape}, defaults to the default * BasicStroke.

**/ protected final Stroke stroke; /** generated from {@link #shape} and {@link #stroke} **/ protected final Shape strokedShape; /** *

The current rotation (CCW radians) and translation taking this shape * from local to canvas coordinates.

**/ protected float theta = 0.0f, tx = 0.0f, ty = 0.0f; /** *

The current transform corresponding to {@link #theta}, {@link #tx}, * and {@link #ty}.

**/ protected final AffineTransform xform = new AffineTransform(); /** *

Sets all fields.

* * @param shape the shape to draw, not null * @param stroke the stroke to use, or null for default * @param paint the paint to use, or null for default * @param filled whether to fill the shape **/ public Geom(Shape shape, Stroke stroke, Paint paint, boolean filled) { if (shape == null) throw new IllegalArgumentException("null shape"); this.shape = shape; if (stroke == null) stroke = new BasicStroke(); this.stroke = stroke; this.strokedShape = stroke.createStrokedShape(shape); if (paint == null) paint = new Color(0, 0, 0); this.paint = paint; this.filled = filled; } /** use default fill **/ public Geom(Shape shape, Stroke stroke, Paint paint) { this(shape, stroke, paint, false); } /** use default stroke and paint **/ public Geom(Shape shape, boolean filled) { this(shape, null, null, filled); } /** use default stroke and fill **/ public Geom(Shape shape, Paint paint) { this(shape, null, paint); } /** use default paint and fill **/ public Geom(Shape shape, Stroke stroke) { this(shape, stroke, null); } /** use default stroke, paint, and fill **/ public Geom(Shape shape) { this(shape, false); } /** incrementally translate this Geom **/ public synchronized void translate(float dx, float dy) { tx += dx; ty += dy; updateXform(); } /** incrementally rotate this Geom by the given angle in CCW degrees **/ public synchronized void rotate(float dThetaDegrees) { theta += Math.toRadians(dThetaDegrees); updateXform(); } /** *

Helper called by {@link #rotate} and {@link #translate} to update * {@link #xform}.

**/ protected synchronized void updateXform() { float c = (float) Math.cos(theta), s = (float) Math.sin(theta); xform.setTransform(c, s, -s, c, tx, ty); } /** *

Check if the given point in canvas coordinates is contained by this * Geom, in its current transformed location.

**/ public synchronized boolean contains(Point2D.Float p) { //make a copy so that passed point is not modified Point2D.Float q = new Point2D.Float((float) p.getX(), (float) p.getY()); try { xform.inverseTransform(q, q); } catch (NoninvertibleTransformException ex) { assert(false); } return shape.contains(q) || strokedShape.contains(q); } /** draw this Geom **/ public synchronized void draw(Graphics2D g2d) { //shouldn't happen in current code, but if you modify it... if (shape == null) return; Stroke strokeWas = null; strokeWas = g2d.getStroke(); g2d.setStroke(stroke); Paint paintWas = null; if (paint != null) { paintWas = g2d.getPaint(); g2d.setPaint(paint); } Shape s = (xform != null) ? xform.createTransformedShape(shape) : shape; if (filled) g2d.fill(s); else g2d.draw(s); if (paint != null) g2d.setPaint(paintWas); if (stroke != null) g2d.setStroke(strokeWas); } } /** all {@link Geom}s currently in the drawing **/ protected java.util.List geoms = new ArrayList(); /** *

An on-screen canvas that knows how to move and draw {@link * HW3_Sample#geoms}.

* *

By default, the origin of canvas coordinate frame is in the upper left, * X is positive right, and Y is positive down.

**/ public class World extends JComponent { /** current canvas pan and zoom **/ protected float panX = 0.0f, panY = 0.0f, zoom = 1.0f; /** driven by {@link #panX}, {@link #panY}, and {@link #zoom} **/ protected AffineTransform navXForm = new AffineTransform(); /** handles mouse events **/ protected MouseAdapter mouseListener = new MouseAdapter() { /** currently dragged Geom, if any **/ protected Geom dragGeom = null; /** location of last event **/ protected int lastX, lastY; /** *

Whether we are currently processing a navigation drag.

* *

It is not quite sufficient to check {@link #dragGeom} null to * determine if navigating. Consider {@link #mouseExited}.

**/ protected boolean navigating = false; /** checks if any geom was picked **/ public void mousePressed(MouseEvent e) { lastX = e.getX(); lastY = e.getY(); Point2D.Float p = new Point2D.Float(lastX, lastY); try { navXForm.inverseTransform(p, p); } catch (NoninvertibleTransformException ex) { assert(false); } synchronized (geoms) { for (Iterator it = geoms.iterator(); it.hasNext(); ) { Geom g = it.next(); if (g.contains(p)) { dragGeom = g; break; } } } if (dragGeom == null) navigating = true; } /** *

Incrementally translates or rotates {@link #dragGeom}, if * any.

**/ public void mouseDragged(MouseEvent e) { int x = e.getX(), y = e.getY(); float dx = (x-lastX)/zoom, dy = (y-lastY)/zoom; if (dragGeom != null) { if (e.isShiftDown()) dragGeom.rotate(dx*ROT_DEG_PER_PIXEL); else dragGeom.translate(dx, dy); repaint(); } else if (navigating) { if (e.isControlDown()) zoom(dx*ZOOM_PER_PIXEL); else pan(dx, dy); repaint(); } lastX = x; lastY = y; } /** *

Releases {@link #dragGeom}, if any, and stops {@link * #navigating}.

**/ public void mouseReleased(MouseEvent e) { dragGeom = null; navigating = false; } /** *

Releases {@link #dragGeom}, if any, and stops {@link * #navigating}.

**/ public void mouseExited(MouseEvent e) { dragGeom = null; navigating = false; } }; /** *

Constructs a new canvas, sets its preferred size based on {@link * HW3_Sample#WINDOW_SIZE}, and adds {@link #mouseListener}.

**/ public World() { setPreferredSize(new Dimension(WINDOW_SIZE, WINDOW_SIZE)); addMouseListener(mouseListener); addMouseMotionListener(mouseListener); } /** *

This is the correct method to override to paint the inside of a Swing * component.

* *

This implementation clears to {@link #BACKGROUND_COLOR} and then * paints all {@link #geoms}.

**/ public void paintComponent(Graphics g) { Graphics2D g2d = (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setBackground(BACKGROUND_COLOR); g2d.clearRect(0, 0, getWidth(), getHeight()); g2d.setTransform(navXForm); synchronized (geoms) { for (Iterator it = geoms.iterator(); it.hasNext(); ) (it.next()).draw(g2d); } } /** incrementally set the canvas pan **/ public void pan(float dx, float dy) { panX += dx; panY += dy; updateNavXForm(); } /** incrementally set the canvas zoom **/ public void zoom(float dz) { zoom += dz; if (zoom < MIN_ZOOM) zoom = MIN_ZOOM; if (zoom > MAX_ZOOM) zoom = MAX_ZOOM; updateNavXForm(); } /** *

Helper called by {@link #pan} and {@link #zoom} to update * {@link #navXForm}.

**/ protected void updateNavXForm() { navXForm.setTransform(zoom, 0.0f, 0.0f, zoom, panX, panY); } } /** adds a few sample {@link Geom}s to {@link #geoms} **/ protected void addSampleGeoms() { synchronized (geoms) { Geom r = new Geom(new RoundRectangle2D.Float(0.0f, 0.0f, 100.0f, 50.0f, 10.0f, 10.0f), new BasicStroke(3.0f), Color.RED, true); r.translate(50.0f, 50.0f); r.rotate(30.0f); geoms.add(r); Geom c = new Geom(new CubicCurve2D.Float(0.0f, 0.0f, 50.0f, -50.0f, 100.0f, -50.0f, 150.0f, 0.0f), new BasicStroke(8.0f)); c.translate(200.0f, 250.0f); geoms.add(c); } } /** *

Constructor makes and packs the top-level JFrame.

* *

The frame contains a {@link World} which displays {@link #geoms}. The * latter is populated by {@link #addSampleGeoms}.

**/ public HW3_Sample() { JFrame f = new JFrame("HW3 Sample"); //shows in frame title bar //make the program exit when the window close button is clicked f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); addSampleGeoms(); Container c = f.getContentPane(); c.add(new World()); //shrink-wrap the frame around the canvas at its preferred size f.pack(); //make the frame visible f.setVisible(true); System.out.println("click and drag to translate any object"); System.out.println( "click and drag horizontally with shift to rotate any object"); System.out.println("click and drag on the background to pan"); System.out.println( "click and drag horizontally on the background with ctrl to zoom"); } /** Constructs a new {@link HW3_Sample} **/ public static void main(String[] arg) { //since we'll be changing swing layout, do all the work inside the AWT //event thread SwingUtilities.invokeLater( new Runnable() { public void run() { new HW3_Sample(); } } ); } }