/*************************************************************************
* Compilation: javac Draw.java
*
* Simple graphics library.
*
*
* Remarks
* -------
* - lines are much faster than spots?
* - careful using setFont in inner loop within an animation -
* it can cause flicker
*
*************************************************************************/
import javax.swing.*;
import java.awt.*;
import java.awt.geom.*;
import java.net.*;
import java.applet.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.ImageIO;
import java.awt.event.*;
import java.util.ArrayList;
public class Draw implements MouseListener, MouseMotionListener, KeyListener, ActionListener {
// listeners
private ArrayList<DrawListener> listeners = new ArrayList<DrawListener>();
// default colors
public final Color DEFAULT_PEN_COLOR = Color.BLACK;
public final Color DEFAULT_CLEAR_COLOR = Color.WHITE;
// current pen color
private Color penColor;
// default canvas size is DEFAULT_SIZE-by-DEFAULT_SIZE
private static final int DEFAULT_SIZE = 512;
private int width = DEFAULT_SIZE;
private int height = DEFAULT_SIZE;
// default pen radius
private static final double DEFAULT_PEN_RADIUS = 0.002;
// current pen radius
private double penRadius;
// show we draw immediately or wait until next show?
private boolean defer = false;
// boundary of drawing canvas, 5% border
private final double BORDER = 0.05;
private final double DEFAULT_XMIN = 0.0;
private final double DEFAULT_XMAX = 1.0;
private final double DEFAULT_YMIN = 0.0;
private final double DEFAULT_YMAX = 1.0;
private double xmin, ymin, xmax, ymax;
// default font
private static final Font DEFAULT_FONT = new Font("Serif", Font.PLAIN, 16);
// current font
private Font font;
// double buffered graphics
private final BufferedImage offscreenImage, onscreenImage;
private final Graphics2D offscreen, onscreen;
// the frame for drawing to the screen
private JFrame frame = new JFrame();
// the label
private JLabel draw;
// create a new drawing region of given dimensions
public Draw() { this(DEFAULT_SIZE, DEFAULT_SIZE); }
public Draw(int width, int height) {
this.width = width;
this.height = height;
if (width <= 0 || height <= 0) throw new RuntimeException("Illegal dimension");
offscreenImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
onscreenImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
offscreen = offscreenImage.createGraphics();
onscreen = onscreenImage.createGraphics();
setXscale();
setYscale();
offscreen.setColor(DEFAULT_CLEAR_COLOR);
offscreen.fillRect(0, 0, width, height);
setPenColor();
setPenRadius();
setFont();
// add antialiasing
RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
offscreen.addRenderingHints(hints);
// the drawing panel
ImageIcon icon = new ImageIcon(onscreenImage);
draw = new JLabel(icon);
draw.addMouseListener(this);
draw.addMouseMotionListener(this);
frame.setContentPane(draw);
// label cannot get keyboard focus
frame.addKeyListener(this);
frame.setResizable(false);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // closes all windows
// frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); // closes only current window
frame.setTitle("Standard Draw");
frame.setJMenuBar(createMenuBar());
frame.pack();
frame.setVisible(true);
clear();
}
// create the menu bar
public JMenuBar createMenuBar() {
JMenuBar menuBar = new JMenuBar();
JMenu menu = new JMenu("File");
menuBar.add(menu);
JMenuItem menuItem1 = new JMenuItem(" Save... ");
menuItem1.addActionListener(this);
menuItem1.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
menu.add(menuItem1);
return menuBar;
}
// create the menu bar
public JLabel getJLabel() { return draw; }
// change the user coordinate system
public void setXscale() { setXscale(DEFAULT_XMIN, DEFAULT_XMAX); }
public void setYscale() { setYscale(DEFAULT_YMIN, DEFAULT_YMAX); }
public void setXscale(double min, double max) {
double size = max - min;
xmin = min - BORDER * size;
xmax = max + BORDER * size;
}
public void setYscale(double min, double max) {
double size = max - min;
ymin = min - BORDER * size;
ymax = max + BORDER * size;
}
// helper functions that scale from user coordinates to screen coordinates and back
private double scaleX (double x) { return width * (x - xmin) / (xmax - xmin); }
private double scaleY (double y) { return height * (ymax - y) / (ymax - ymin); }
private double factorX(double w) { return w * width / Math.abs(xmax - xmin); }
private double factorY(double h) { return h * height / Math.abs(ymax - ymin); }
private double userX (double x) { return xmin + x * (xmax - xmin) / width; }
private double userY (double y) { return ymax - y * (ymax - ymin) / height; }
// clear the screen with given color
public void clear() { clear(DEFAULT_CLEAR_COLOR); }
public void clear(Color color) {
offscreen.setColor(color);
offscreen.fillRect(0, 0, width, height);
offscreen.setColor(penColor);
show();
}
// set the pen size
public void setPenRadius() { setPenRadius(DEFAULT_PEN_RADIUS); }
public void setPenRadius(double r) {
penRadius = r * DEFAULT_SIZE;
BasicStroke stroke = new BasicStroke((float) penRadius, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
offscreen.setStroke(stroke);
}
// set the pen color
public void setPenColor() { setPenColor(DEFAULT_PEN_COLOR); }
public void setPenColor(Color color) {
penColor = color;
offscreen.setColor(penColor);
}
// write the given string in the current font
public void setFont() { setFont(DEFAULT_FONT); }
public void setFont(Font f) { font = f; }
// draw a line from (x0, y0) to (x1, y1)
public void line(double x0, double y0, double x1, double y1) {
offscreen.draw(new Line2D.Double(scaleX(x0), scaleY(y0), scaleX(x1), scaleY(y1)));
show();
}
// draw one pixel at (x, y)
private void pixel(double x, double y) {
offscreen.fillRect((int) Math.round(scaleX(x)), (int) Math.round(scaleY(y)), 1, 1);
}
// draw point at (x, y)
public void point(double x, double y) {
double xs = scaleX(x);
double ys = scaleY(y);
double r = penRadius;
// double ws = factorX(2*r);
// double hs = factorY(2*r);
// if (ws <= 1 && hs <= 1) pixel(x, y);
if (r <= 1) pixel(x, y);
else offscreen.fill(new Ellipse2D.Double(xs - r/2, ys - r/2, r, r));
show();
}
// draw circle of radius r, centered on (x, y); degenerate to pixel if small
public void circle(double x, double y, double r) {
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*r);
double hs = factorY(2*r);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.draw(new Ellipse2D.Double(xs - ws/2, ys - hs/2, ws, hs));
show();
}
// draw arc of radius r, centered on (x, y), from angle1 to angle2 (in degrees)
public void arc(double x, double y, double r, double angle1, double angle2) {
while (angle2 < angle1) angle2 += 360;
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*r);
double hs = factorY(2*r);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.draw(new Arc2D.Double(xs - ws/2, ys - hs/2, ws, hs, angle1, angle2 - angle1, Arc2D.OPEN));
show();
}
// draw filled circle of radius r, centered on (x, y); degenerate to pixel if small
public void filledCircle(double x, double y, double r) {
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*r);
double hs = factorY(2*r);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.fill(new Ellipse2D.Double(xs - ws/2, ys - hs/2, ws, hs));
show();
}
// draw squared of side length 2r, centered on (x, y); degenerate to pixel if small
public void square(double x, double y, double r) {
// screen coordinates
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*r);
double hs = factorY(2*r);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.draw(new Rectangle2D.Double(xs - ws/2, ys - hs/2, ws, hs));
show();
}
// draw squared of side length 2r, centered on (x, y); degenerate to pixel if small
public void filledSquare(double x, double y, double r) {
// screen coordinates
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*r);
double hs = factorY(2*r);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.fill(new Rectangle2D.Double(xs - ws/2, ys - hs/2, ws, hs));
show();
}
// draw a polygon with the given (x[i], y[i]) coordinates
public void polygon(double[] x, double[] y) {
int N = x.length;
GeneralPath path = new GeneralPath();
path.moveTo((float) scaleX(x[0]), (float) scaleY(y[0]));
for (int i = 0; i < N; i++)
path.lineTo((float) scaleX(x[i]), (float) scaleY(y[i]));
path.closePath();
offscreen.draw(path);
show();
}
// draw a filled polygon with the given (x[i], y[i]) coordinates
public void filledPolygon(double[] x, double[] y) {
int N = x.length;
GeneralPath path = new GeneralPath();
path.moveTo((float) scaleX(x[0]), (float) scaleY(y[0]));
for (int i = 0; i < N; i++)
path.lineTo((float) scaleX(x[i]), (float) scaleY(y[i]));
path.closePath();
offscreen.fill(path);
show();
}
// get an image from the given filename
private Image getImage(String filename) {
// to read from file
ImageIcon icon = new ImageIcon(filename);
// try to read from URL
if ((icon == null) || (icon.getImageLoadStatus() != MediaTracker.COMPLETE)) {
try {
URL url = new URL(filename);
icon = new ImageIcon(url);
} catch (Exception e) { /* not a url */ }
}
// in case file is inside a .jar
if ((icon == null) || (icon.getImageLoadStatus() != MediaTracker.COMPLETE)) {
URL url = Draw.class.getResource(filename);
if (url == null) throw new RuntimeException("image " + filename + " not found");
icon = new ImageIcon(url);
}
return icon.getImage();
}
// draw picture (gif, jpg, or png) centered on (x, y)
public void picture(double x, double y, String s) {
Image image = getImage(s);
double xs = scaleX(x);
double ys = scaleY(y);
int ws = image.getWidth(null);
int hs = image.getHeight(null);
if (ws < 0 || hs < 0) throw new RuntimeException("image " + s + " is corrupt");
offscreen.drawImage(image, (int) Math.round(xs - ws/2.0), (int) Math.round(ys - hs/2.0), null);
show();
}
// draw picture (gif, jpg, or png) centered on (x, y), rescaled to w-by-h
public void picture(double x, double y, String s, double w, double h) {
Image image = getImage(s);
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(w);
double hs = factorY(h);
if (ws <= 1 && hs <= 1) pixel(x, y);
else {
offscreen.drawImage(image, (int) Math.round(xs - ws/2.0),
(int) Math.round(ys - hs/2.0),
(int) Math.round(ws),
(int) Math.round(hs), null);
}
show();
}
// write the given text string in the current font, center on (x, y)
public void text(double x, double y, String s) {
offscreen.setFont(font);
FontMetrics metrics = offscreen.getFontMetrics();
double xs = scaleX(x);
double ys = scaleY(y);
int ws = metrics.stringWidth(s);
int hs = metrics.getDescent();
offscreen.drawString(s, (float) (xs - ws/2.0), (float) (ys + hs));
show();
}
// display on screen and pause for t miliseconds
public void show(int t) {
defer = true;
onscreen.drawImage(offscreenImage, 0, 0, null);
frame.repaint();
try { Thread.currentThread().sleep(t); }
catch (InterruptedException e) { System.out.println("Error sleeping"); }
}
// view on-screen, creating new frame if necessary
public void show() {
if (!defer) onscreen.drawImage(offscreenImage, 0, 0, null);
if (!defer) frame.repaint();
}
// save to file - suffix must be png, jpg, or gif
public void save(String filename) {
File file = new File(filename);
String suffix = filename.substring(filename.lastIndexOf('.') + 1);
// png files
if (suffix.toLowerCase().equals("png")) {
try { ImageIO.write(offscreenImage, suffix, file); }
catch (IOException e) { e.printStackTrace(); }
}
// need to change from ARGB to RGB for jpeg
// reference: http://archives.java.sun.com/cgi-bin/wa?A2=ind0404&L=java2d-interest&D=0&P=2727
else if (suffix.toLowerCase().equals("jpg")) {
WritableRaster raster = offscreenImage.getRaster();
WritableRaster newRaster;
newRaster = raster.createWritableChild(0, 0, width, height, 0, 0, new int[] {0, 1, 2});
DirectColorModel cm = (DirectColorModel) offscreenImage.getColorModel();
DirectColorModel newCM = new DirectColorModel(cm.getPixelSize(),
cm.getRedMask(),
cm.getGreenMask(),
cm.getBlueMask());
BufferedImage rgbBuffer = new BufferedImage(newCM, newRaster, false, null);
try { ImageIO.write(rgbBuffer, suffix, file); }
catch (IOException e) { e.printStackTrace(); }
}
else {
System.out.println("Invalid image file type: " + suffix);
}
}
// open a save dialog when the user selects "Save As" from the menu
public void actionPerformed(ActionEvent e) {
FileDialog chooser = new FileDialog(frame, "Use a .png or .jpg extension", FileDialog.SAVE);
chooser.setVisible(true);
String filename = chooser.getFile();
if (filename != null) {
save(chooser.getDirectory() + File.separator + chooser.getFile());
}
}
public void addListener(DrawListener listener) {
listeners.add(listener);
}
// user types a key
public void keyTyped(KeyEvent e) {
for (DrawListener listener : listeners)
listener.keyTyped(e.getKeyChar());
}
// user presses mouse button #1
public void mousePressed(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
for (DrawListener listener : listeners)
listener.mousePressed(userX(e.getX()), userY(e.getY()));
}
}
// user releases mouse button #1
public void mouseReleased(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
for (DrawListener listener : listeners)
listener.mouseReleased(userX(e.getX()), userY(e.getY()));
}
}
// user drags the mouse
public void mouseDragged(MouseEvent e) {
// seems to not recognize mouse dragging events if we specify a button???
// if (e.getButton() == MouseEvent.BUTTON1)
for (DrawListener listener : listeners)
listener.mouseDragged(userX(e.getX()), userY(e.getY()));
}
// we don't support these methods
public void keyPressed (KeyEvent e) { }
public void keyReleased (KeyEvent e) { }
public void mouseMoved (MouseEvent e) { }
public void mouseEntered (MouseEvent e) { }
public void mouseExited (MouseEvent e) { }
public void mouseClicked (MouseEvent e) { }
// test client
public static void main(String[] args) {
Draw draw = new Draw();
draw.square(.2, .8, .1);
draw.filledSquare(.8, .8, .2);
draw.circle(.8, .2, .2);
draw.setPenColor(Color.MAGENTA);
draw.setPenRadius(.02);
draw.arc(.8, .2, .1, 200, 45);
// draw a blue diamond
draw.setPenRadius();
draw.setPenColor(Color.BLUE);
double[] x = { .1, .2, .3, .2 };
double[] y = { .2, .3, .2, .1 };
draw.filledPolygon(x, y);
// text
draw.text(0.8, 0.2, "centered");
}
}