
edu.princeton.cs.algs4.Draw Maven / Gradle / Ivy
Show all versions of algorithm Show documentation
/******************************************************************************
* Compilation: javac Draw.java
* Execution: java Draw
* Dependencies: none
*
* Drawing library. This class provides a basic capability for creating
* drawings with your programs. It uses a simple graphics model that
* allows you to create drawings consisting of points, lines, and curves
* in a window on your computer and to save the drawings to a file.
* This is the object-oriented version of standard draw; it supports
* multiple indepedent drawing windows.
*
* Todo
* ----
* - Add support for gradient fill, etc.
*
* Remarks
* -------
* - don't use AffineTransform for rescaling since it inverts
* images and strings
* - careful using setFont in inner loop within an animation -
* it can cause flicker
*
******************************************************************************/
package edu.princeton.cs.algs4;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.FileDialog;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.DirectColorModel;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.TreeSet;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.KeyStroke;
/**
* Draw. This class provides a basic capability for
* creating drawings with your programs. It uses a simple graphics model that
* allows you to create drawings consisting of points, lines, and curves
* in a window on your computer and to save the drawings to a file.
* This is the object-oriented version of standard draw; it supports
* multiple indepedent drawing windows.
*
* For additional documentation, see Section 3.1 of
* Computer Science: An Interdisciplinary Approach by Robert Sedgewick and Kevin Wayne.
*
* @author Robert Sedgewick
* @author Kevin Wayne
*/
public final class Draw implements ActionListener, MouseListener, MouseMotionListener, KeyListener {
/**
* The color black.
*/
public static final Color BLACK = Color.BLACK;
/**
* The color blue.
*/
public static final Color BLUE = Color.BLUE;
/**
* The color cyan.
*/
public static final Color CYAN = Color.CYAN;
/**
* The color dark gray.
*/
public static final Color DARK_GRAY = Color.DARK_GRAY;
/**
* The color gray.
*/
public static final Color GRAY = Color.GRAY;
/**
* The color green.
*/
public static final Color GREEN = Color.GREEN;
/**
* The color light gray.
*/
public static final Color LIGHT_GRAY = Color.LIGHT_GRAY;
/**
* The color magenta.
*/
public static final Color MAGENTA = Color.MAGENTA;
/**
* The color orange.
*/
public static final Color ORANGE = Color.ORANGE;
/**
* The color pink.
*/
public static final Color PINK = Color.PINK;
/**
* The color red.
*/
public static final Color RED = Color.RED;
/**
* The color white.
*/
public static final Color WHITE = Color.WHITE;
/**
* The color yellow.
*/
public static final Color YELLOW = Color.YELLOW;
/**
* Shade of blue used in Introduction to Programming in Java.
* It is Pantone 300U. The RGB values are approximately (9, 90, 166).
*/
public static final Color BOOK_BLUE = new Color(9, 90, 166);
/**
* Shade of light blue used in Introduction to Programming in Java.
* The RGB values are approximately (103, 198, 243).
*/
public static final Color BOOK_LIGHT_BLUE = new Color(103, 198, 243);
/**
* Shade of red used in Algorithms, 4th edition.
* It is Pantone 1805U. The RGB values are approximately (150, 35, 31).
*/
public static final Color BOOK_RED = new Color(150, 35, 31);
/**
* Shade of orange used in Princeton's identity.
* It is PMS 158. The RGB values are approximately (245, 128, 37).
*/
public static final Color PRINCETON_ORANGE = new Color(245, 128, 37);
// default colors
private static final Color DEFAULT_PEN_COLOR = BLACK;
private static final Color DEFAULT_CLEAR_COLOR = WHITE;
// boundary of drawing canvas, 0% border
private static final double BORDER = 0.0;
private static final double DEFAULT_XMIN = 0.0;
private static final double DEFAULT_XMAX = 1.0;
private static final double DEFAULT_YMIN = 0.0;
private static final double DEFAULT_YMAX = 1.0;
// default canvas size is SIZE-by-SIZE
private static final int DEFAULT_SIZE = 512;
// default pen radius
private static final double DEFAULT_PEN_RADIUS = 0.002;
// default font
private static final Font DEFAULT_FONT = new Font("SansSerif", Font.PLAIN, 16);
// current pen color
private Color penColor;
// canvas size
private int width = DEFAULT_SIZE;
private int height = DEFAULT_SIZE;
// current pen radius
private double penRadius;
// show we draw immediately or wait until next show?
private boolean defer = false;
private double xmin, ymin, xmax, ymax;
// name of window
private String name = "Draw";
// for synchronization
private final Object mouseLock = new Object();
private final Object keyLock = new Object();
// current font
private Font font;
// the JLabel for drawing
private JLabel draw;
// double buffered graphics
private BufferedImage offscreenImage, onscreenImage;
private Graphics2D offscreen, onscreen;
// the frame for drawing to the screen
private JFrame frame = new JFrame();
// mouse state
private boolean isMousePressed = false;
private double mouseX = 0;
private double mouseY = 0;
// keyboard state
private final LinkedList keysTyped = new LinkedList();
private final TreeSet keysDown = new TreeSet();
// event-based listeners
private final ArrayList listeners = new ArrayList();
/**
* Initializes an empty drawing object with the given name.
*
* @param name the title of the drawing window.
*/
public Draw(String name) {
this.name = name;
init();
}
/**
* Initializes an empty drawing object.
*/
public Draw() {
init();
}
private void init() {
if (frame != null) frame.setVisible(false);
frame = new JFrame();
offscreenImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
onscreenImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
offscreen = offscreenImage.createGraphics();
onscreen = onscreenImage.createGraphics();
setXscale();
setYscale();
offscreen.setColor(DEFAULT_CLEAR_COLOR);
offscreen.fillRect(0, 0, width, height);
setPenColor();
setPenRadius();
setFont();
clear();
// 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);
// frame stuff
ImageIcon icon = new ImageIcon(onscreenImage);
draw = new JLabel(icon);
draw.addMouseListener(this);
draw.addMouseMotionListener(this);
frame.setContentPane(draw);
frame.addKeyListener(this); // JLabel cannot get keyboard focus
frame.setResizable(false);
// frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // closes all windows
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); // closes only current window
frame.setFocusTraversalKeysEnabled(false); // to recognize VK_TAB with isKeyPressed()
frame.setTitle(name);
frame.setJMenuBar(createMenuBar());
frame.pack();
frame.requestFocusInWindow();
frame.setVisible(true);
}
/**
* Sets the upper-left hand corner of the drawing window to be (x, y), where (0, 0) is upper left.
*
* @param x the number of pixels from the left
* @param y the number of pixels from the top
* @throws IllegalArgumentException if the width or height is 0 or negative
*/
public void setLocationOnScreen(int x, int y) {
if (x <= 0 || y <= 0) throw new IllegalArgumentException();
frame.setLocation(x, y);
}
/**
* Sets the default close operation.
*
* @param value the value, typically {@code JFrame.EXIT_ON_CLOSE}
* (close all windows) or {@code JFrame.DISPOSE_ON_CLOSE}
* (close current window)
*/
public void setDefaultCloseOperation(int value) {
frame.setDefaultCloseOperation(value);
}
/**
* Sets the canvas (drawing area) to be width-by-height pixels.
* This also erases the current drawing and resets the coordinate system, pen radius,
* pen color, and font back to their default values.
* Ordinarly, this method is called once, at the very beginning of a program.
*
* @param canvasWidth the width as a number of pixels
* @param canvasHeight the height as a number of pixels
* @throws IllegalArgumentException unless both {@code canvasWidth}
* and {@code canvasHeight} are positive
*/
public void setCanvasSize(int canvasWidth, int canvasHeight) {
if (canvasWidth < 1 || canvasHeight < 1) {
throw new IllegalArgumentException("width and height must be positive");
}
width = canvasWidth;
height = canvasHeight;
init();
}
// create the menu bar (changed to private)
private 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;
}
/***************************************************************************
* User and screen coordinate systems.
***************************************************************************/
/**
* Sets the x-scale to be the default (between 0.0 and 1.0).
*/
public void setXscale() {
setXscale(DEFAULT_XMIN, DEFAULT_XMAX);
}
/**
* Sets the y-scale to be the default (between 0.0 and 1.0).
*/
public void setYscale() {
setYscale(DEFAULT_YMIN, DEFAULT_YMAX);
}
/**
* Sets the x-scale.
*
* @param min the minimum value of the x-scale
* @param max the maximum value of the x-scale
*/
public void setXscale(double min, double max) {
double size = max - min;
xmin = min - BORDER * size;
xmax = max + BORDER * size;
}
/**
* Sets the y-scale.
*
* @param min the minimum value of the y-scale
* @param max the maximum value of the y-scale
*/
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; }
/**
* Clears the screen to the default color (white).
*/
public void clear() {
clear(DEFAULT_CLEAR_COLOR);
}
/**
* Clears the screen to the given color.
*
* @param color the color to make the background
*/
public void clear(Color color) {
offscreen.setColor(color);
offscreen.fillRect(0, 0, width, height);
offscreen.setColor(penColor);
draw();
}
/**
* Gets the current pen radius.
*
* @return the current pen radius
*/
public double getPenRadius() {
return penRadius;
}
/**
* Sets the pen size to the default (.002).
*/
public void setPenRadius() {
setPenRadius(DEFAULT_PEN_RADIUS);
}
/**
* Sets the radius of the pen to the given size.
*
* @param r the radius of the pen
* @throws IllegalArgumentException if r is negative
*/
public void setPenRadius(double r) {
if (r < 0) throw new IllegalArgumentException("pen radius must be positive");
penRadius = r * DEFAULT_SIZE;
BasicStroke stroke = new BasicStroke((float) penRadius, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
// BasicStroke stroke = new BasicStroke((float) penRadius);
offscreen.setStroke(stroke);
}
/**
* Gets the current pen color.
*
* @return the current pen color
*/
public Color getPenColor() {
return penColor;
}
/**
* Sets the pen color to the default color (black).
*/
public void setPenColor() {
setPenColor(DEFAULT_PEN_COLOR);
}
/**
* Sets the pen color to the given color.
*
* @param color the color to make the pen
*/
public void setPenColor(Color color) {
penColor = color;
offscreen.setColor(penColor);
}
/**
* Sets the pen color to the given RGB color.
*
* @param red the amount of red (between 0 and 255)
* @param green the amount of green (between 0 and 255)
* @param blue the amount of blue (between 0 and 255)
* @throws IllegalArgumentException if the amount of red, green, or blue are outside prescribed range
*/
public void setPenColor(int red, int green, int blue) {
if (red < 0 || red >= 256) throw new IllegalArgumentException("amount of red must be between 0 and 255");
if (green < 0 || green >= 256) throw new IllegalArgumentException("amount of red must be between 0 and 255");
if (blue < 0 || blue >= 256) throw new IllegalArgumentException("amount of red must be between 0 and 255");
setPenColor(new Color(red, green, blue));
}
/**
* Turns on xor mode.
*/
public void xorOn() {
offscreen.setXORMode(DEFAULT_CLEAR_COLOR);
}
/**
* Turns off xor mode.
*/
public void xorOff() {
offscreen.setPaintMode();
}
/**
* Gets the current {@code JLabel} for use in some other GUI.
*
* @return the current {@code JLabel}
*/
public JLabel getJLabel() {
return draw;
}
/**
* Gets the current font.
*
* @return the current font
*/
public Font getFont() {
return font;
}
/**
* Sets the font to the default font (sans serif, 16 point).
*/
public void setFont() {
setFont(DEFAULT_FONT);
}
/**
* Sets the font to the given value.
*
* @param font the font
*/
public void setFont(Font font) {
this.font = font;
}
/***************************************************************************
* Drawing geometric shapes.
***************************************************************************/
/**
* Draws a line from (x0, y0) to (x1, y1).
*
* @param x0 the x-coordinate of the starting point
* @param y0 the y-coordinate of the starting point
* @param x1 the x-coordinate of the destination point
* @param y1 the y-coordinate of the destination point
*/
public void line(double x0, double y0, double x1, double y1) {
offscreen.draw(new Line2D.Double(scaleX(x0), scaleY(y0), scaleX(x1), scaleY(y1)));
draw();
}
/**
* Draws one pixel at (x, y).
*
* @param x the x-coordinate of the pixel
* @param y the y-coordinate of the pixel
*/
private void pixel(double x, double y) {
offscreen.fillRect((int) Math.round(scaleX(x)), (int) Math.round(scaleY(y)), 1, 1);
}
/**
* Draws a point at (x, y).
*
* @param x the x-coordinate of the point
* @param y the y-coordinate of the point
*/
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));
draw();
}
/**
* Draws a circle of radius r, centered on (x, y).
*
* @param x the x-coordinate of the center of the circle
* @param y the y-coordinate of the center of the circle
* @param r the radius of the circle
* @throws IllegalArgumentException if the radius of the circle is negative
*/
public void circle(double x, double y, double r) {
if (r < 0) throw new IllegalArgumentException("circle radius can't be negative");
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));
draw();
}
/**
* Draws a filled circle of radius r, centered on (x, y).
*
* @param x the x-coordinate of the center of the circle
* @param y the y-coordinate of the center of the circle
* @param r the radius of the circle
* @throws IllegalArgumentException if the radius of the circle is negative
*/
public void filledCircle(double x, double y, double r) {
if (r < 0) throw new IllegalArgumentException("circle radius can't be negative");
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));
draw();
}
/**
* Draws an ellipse with given semimajor and semiminor axes, centered on (x, y).
*
* @param x the x-coordinate of the center of the ellipse
* @param y the y-coordinate of the center of the ellipse
* @param semiMajorAxis is the semimajor axis of the ellipse
* @param semiMinorAxis is the semiminor axis of the ellipse
* @throws IllegalArgumentException if either of the axes are negative
*/
public void ellipse(double x, double y, double semiMajorAxis, double semiMinorAxis) {
if (semiMajorAxis < 0) throw new IllegalArgumentException("ellipse semimajor axis can't be negative");
if (semiMinorAxis < 0) throw new IllegalArgumentException("ellipse semiminor axis can't be negative");
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*semiMajorAxis);
double hs = factorY(2*semiMinorAxis);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.draw(new Ellipse2D.Double(xs - ws/2, ys - hs/2, ws, hs));
draw();
}
/**
* Draws an ellipse with given semimajor and semiminor axes, centered on (x, y).
* @param x the x-coordinate of the center of the ellipse
* @param y the y-coordinate of the center of the ellipse
* @param semiMajorAxis is the semimajor axis of the ellipse
* @param semiMinorAxis is the semiminor axis of the ellipse
* @throws IllegalArgumentException if either of the axes are negative
*/
public void filledEllipse(double x, double y, double semiMajorAxis, double semiMinorAxis) {
if (semiMajorAxis < 0) throw new IllegalArgumentException("ellipse semimajor axis can't be negative");
if (semiMinorAxis < 0) throw new IllegalArgumentException("ellipse semiminor axis can't be negative");
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*semiMajorAxis);
double hs = factorY(2*semiMinorAxis);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.fill(new Ellipse2D.Double(xs - ws/2, ys - hs/2, ws, hs));
draw();
}
/**
* Draws an arc of radius r, centered on (x, y), from angle1 to angle2 (in degrees).
*
* @param x the x-coordinate of the center of the circle
* @param y the y-coordinate of the center of the circle
* @param r the radius of the circle
* @param angle1 the starting angle. 0 would mean an arc beginning at 3 o'clock.
* @param angle2 the angle at the end of the arc. For example, if
* you want a 90 degree arc, then angle2 should be angle1 + 90.
* @throws IllegalArgumentException if the radius of the circle is negative
*/
public void arc(double x, double y, double r, double angle1, double angle2) {
if (r < 0) throw new IllegalArgumentException("arc radius can't be negative");
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));
draw();
}
/**
* Draws a square of side length 2r, centered on (x, y).
*
* @param x the x-coordinate of the center of the square
* @param y the y-coordinate of the center of the square
* @param r radius is half the length of any side of the square
* @throws IllegalArgumentException if r is negative
*/
public void square(double x, double y, double r) {
if (r < 0) throw new IllegalArgumentException("square side length can't be negative");
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));
draw();
}
/**
* Draws a filled square of side length 2r, centered on (x, y).
*
* @param x the x-coordinate of the center of the square
* @param y the y-coordinate of the center of the square
* @param r radius is half the length of any side of the square
* @throws IllegalArgumentException if r is negative
*/
public void filledSquare(double x, double y, double r) {
if (r < 0) throw new IllegalArgumentException("square side length can't be negative");
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));
draw();
}
/**
* Draws a rectangle of given half width and half height, centered on (x, y).
*
* @param x the x-coordinate of the center of the rectangle
* @param y the y-coordinate of the center of the rectangle
* @param halfWidth is half the width of the rectangle
* @param halfHeight is half the height of the rectangle
* @throws IllegalArgumentException if halfWidth or halfHeight is negative
*/
public void rectangle(double x, double y, double halfWidth, double halfHeight) {
if (halfWidth < 0) throw new IllegalArgumentException("half width can't be negative");
if (halfHeight < 0) throw new IllegalArgumentException("half height can't be negative");
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*halfWidth);
double hs = factorY(2*halfHeight);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.draw(new Rectangle2D.Double(xs - ws/2, ys - hs/2, ws, hs));
draw();
}
/**
* Draws a filled rectangle of given half width and half height, centered on (x, y).
*
* @param x the x-coordinate of the center of the rectangle
* @param y the y-coordinate of the center of the rectangle
* @param halfWidth is half the width of the rectangle
* @param halfHeight is half the height of the rectangle
* @throws IllegalArgumentException if halfWidth or halfHeight is negative
*/
public void filledRectangle(double x, double y, double halfWidth, double halfHeight) {
if (halfWidth < 0) throw new IllegalArgumentException("half width can't be negative");
if (halfHeight < 0) throw new IllegalArgumentException("half height can't be negative");
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(2*halfWidth);
double hs = factorY(2*halfHeight);
if (ws <= 1 && hs <= 1) pixel(x, y);
else offscreen.fill(new Rectangle2D.Double(xs - ws/2, ys - hs/2, ws, hs));
draw();
}
/**
* Draws a polygon with the given (x[i], y[i]) coordinates.
*
* @param x an array of all the x-coordindates of the polygon
* @param y an array of all the y-coordindates of the polygon
*/
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);
draw();
}
/**
* Draws a filled polygon with the given (x[i], y[i]) coordinates.
*
* @param x an array of all the x-coordindates of the polygon
* @param y an array of all the y-coordindates of the polygon
*/
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);
draw();
}
/***************************************************************************
* Drawing images.
***************************************************************************/
private static BufferedImage getImage(String filename) {
// from a file or URL
try {
URL url = new URL(filename);
return ImageIO.read(url);
}
catch (IOException e) {
// ignore
}
// in case file is inside a .jar (classpath relative to StdDraw)
try {
URL url = StdDraw.class.getResource(filename);
return ImageIO.read(url);
}
catch (IOException e) {
// ignore
}
// in case file is inside a .jar (classpath relative to root of jar)
try {
URL url = StdDraw.class.getResource("/" + filename);
return ImageIO.read(url);
}
catch (IOException e) {
// ignore
}
throw new IllegalArgumentException("image " + filename + " not found");
}
/**
* Draws picture (gif, jpg, or png) centered on (x, y).
*
* @param x the center x-coordinate of the image
* @param y the center y-coordinate of the image
* @param filename the name of the image/picture, e.g., "ball.gif"
* @throws IllegalArgumentException if the image is corrupt
* @throws IllegalArgumentException if {@code filename} is {@code null}
*/
public void picture(double x, double y, String filename) {
if (filename == null) throw new IllegalArgumentException("filename argument is null");
BufferedImage image = getImage(filename);
double xs = scaleX(x);
double ys = scaleY(y);
int ws = image.getWidth();
int hs = image.getHeight();
if (ws < 0 || hs < 0) throw new IllegalArgumentException("image " + filename + " is corrupt");
offscreen.drawImage(image, (int) Math.round(xs - ws/2.0), (int) Math.round(ys - hs/2.0), null);
draw();
}
/**
* Draws picture (gif, jpg, or png) centered on (x, y),
* rotated given number of degrees.
*
* @param x the center x-coordinate of the image
* @param y the center y-coordinate of the image
* @param filename the name of the image/picture, e.g., "ball.gif"
* @param degrees is the number of degrees to rotate counterclockwise
* @throws IllegalArgumentException if the image is corrupt
* @throws IllegalArgumentException if {@code filename} is {@code null}
*/
public void picture(double x, double y, String filename, double degrees) {
if (filename == null) throw new IllegalArgumentException("filename argument is null");
BufferedImage image = getImage(filename);
double xs = scaleX(x);
double ys = scaleY(y);
int ws = image.getWidth();
int hs = image.getHeight();
if (ws < 0 || hs < 0) throw new IllegalArgumentException("image " + filename + " is corrupt");
offscreen.rotate(Math.toRadians(-degrees), xs, ys);
offscreen.drawImage(image, (int) Math.round(xs - ws/2.0), (int) Math.round(ys - hs/2.0), null);
offscreen.rotate(Math.toRadians(+degrees), xs, ys);
draw();
}
/**
* Draws picture (gif, jpg, or png) centered on (x, y), rescaled to w-by-h.
*
* @param x the center x coordinate of the image
* @param y the center y coordinate of the image
* @param filename the name of the image/picture, e.g., "ball.gif"
* @param w the width of the image
* @param h the height of the image
* @throws IllegalArgumentException if the image is corrupt
* @throws IllegalArgumentException if {@code filename} is {@code null}
*/
public void picture(double x, double y, String filename, double w, double h) {
if (filename == null) throw new IllegalArgumentException("filename argument is null");
Image image = getImage(filename);
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(w);
double hs = factorY(h);
if (ws < 0 || hs < 0) throw new IllegalArgumentException("image " + filename + " is corrupt");
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);
}
draw();
}
/**
* Draws picture (gif, jpg, or png) centered on (x, y), rotated
* given number of degrees, rescaled to w-by-h.
*
* @param x the center x-coordinate of the image
* @param y the center y-coordinate of the image
* @param filename the name of the image/picture, e.g., "ball.gif"
* @param w the width of the image
* @param h the height of the image
* @param degrees is the number of degrees to rotate counterclockwise
* @throws IllegalArgumentException if the image is corrupt
* @throws IllegalArgumentException if {@code filename} is {@code null}
*/
public void picture(double x, double y, String filename, double w, double h, double degrees) {
if (filename == null) throw new IllegalArgumentException("filename argument is null");
Image image = getImage(filename);
double xs = scaleX(x);
double ys = scaleY(y);
double ws = factorX(w);
double hs = factorY(h);
if (ws < 0 || hs < 0) throw new IllegalArgumentException("image " + filename + " is corrupt");
if (ws <= 1 && hs <= 1) pixel(x, y);
offscreen.rotate(Math.toRadians(-degrees), xs, ys);
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);
offscreen.rotate(Math.toRadians(+degrees), xs, ys);
draw();
}
/***************************************************************************
* Drawing text.
***************************************************************************/
/**
* Writes the given text string in the current font, centered on (x, y).
*
* @param x the center x-coordinate of the text
* @param y the center y-coordinate of the text
* @param s the text
*/
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));
draw();
}
/**
* Writes the given text string in the current font, centered on (x, y) and
* rotated by the specified number of degrees.
*
* @param x the center x-coordinate of the text
* @param y the center y-coordinate of the text
* @param s the text
* @param degrees is the number of degrees to rotate counterclockwise
*/
public void text(double x, double y, String s, double degrees) {
double xs = scaleX(x);
double ys = scaleY(y);
offscreen.rotate(Math.toRadians(-degrees), xs, ys);
text(x, y, s);
offscreen.rotate(Math.toRadians(+degrees), xs, ys);
}
/**
* Writes the given text string in the current font, left-aligned at (x, y).
*
* @param x the x-coordinate of the text
* @param y the y-coordinate of the text
* @param s the text
*/
public void textLeft(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, (float) (ys + hs));
draw();
}
/**
* Copies the offscreen buffer to the onscreen buffer, pauses for t milliseconds
* and enables double buffering.
* @param t number of milliseconds
* @deprecated replaced by {@link #enableDoubleBuffering()}, {@link #show()}, and {@link #pause(int t)}
*/
@Deprecated
public void show(int t) {
show();
pause(t);
enableDoubleBuffering();
}
/**
* Pause for t milliseconds. This method is intended to support computer animations.
* @param t number of milliseconds
*/
public void pause(int t) {
try {
Thread.sleep(t);
}
catch (InterruptedException e) {
System.out.println("Error sleeping");
}
}
/**
* Copies offscreen buffer to onscreen buffer. There is no reason to call
* this method unless double buffering is enabled.
*/
public void show() {
onscreen.drawImage(offscreenImage, 0, 0, null);
frame.repaint();
}
// draw onscreen if defer is false
private void draw() {
if (!defer) show();
}
/**
* Enable double buffering. All subsequent calls to
* drawing methods such as {@code line()}, {@code circle()},
* and {@code square()} will be deffered until the next call
* to show(). Useful for animations.
*/
public void enableDoubleBuffering() {
defer = true;
}
/**
* Disable double buffering. All subsequent calls to
* drawing methods such as {@code line()}, {@code circle()},
* and {@code square()} will be displayed on screen when called.
* This is the default.
*/
public void disableDoubleBuffering() {
defer = false;
}
/**
* Saves this drawing to a file.
*
* @param filename the name of the file (with suffix png, jpg, or gif)
*/
public void save(String filename) {
File file = new File(filename);
String suffix = filename.substring(filename.lastIndexOf('.') + 1);
// png files
if ("png".equalsIgnoreCase(suffix)) {
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 ("jpg".equalsIgnoreCase(suffix)) {
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);
}
}
/**
* This method cannot be called directly.
*/
@Override
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());
}
}
/***************************************************************************
* Event-based interactions.
***************************************************************************/
/**
* Adds a {@link DrawListener} to listen to keyboard and mouse events.
*
* @param listener the {\tt DrawListener} argument
*/
public void addListener(DrawListener listener) {
// ensure there is a window for listenting to events
show();
listeners.add(listener);
frame.addKeyListener(this);
frame.addMouseListener(this);
frame.addMouseMotionListener(this);
frame.setFocusable(true);
}
/***************************************************************************
* Mouse interactions.
***************************************************************************/
/**
* Returns true if the mouse is being pressed.
*
* @return {@code true} if the mouse is being pressed;
* {@code false} otherwise
*/
public boolean isMousePressed() {
synchronized (mouseLock) {
return isMousePressed;
}
}
/**
* Returns true if the mouse is being pressed.
*
* @return {@code true} if the mouse is being pressed;
* {@code false} otherwise
* @deprecated replaced by {@link #isMousePressed()}
*/
@Deprecated
public boolean mousePressed() {
synchronized (mouseLock) {
return isMousePressed;
}
}
/**
* Returns the x-coordinate of the mouse.
* @return the x-coordinate of the mouse
*/
public double mouseX() {
synchronized (mouseLock) {
return mouseX;
}
}
/**
* Returns the y-coordinate of the mouse.
*
* @return the y-coordinate of the mouse
*/
public double mouseY() {
synchronized (mouseLock) {
return mouseY;
}
}
/**
* This method cannot be called directly.
*/
@Override
public void mouseClicked(MouseEvent e) {
// this body is intentionally left empty
}
/**
* This method cannot be called directly.
*/
@Override
public void mouseEntered(MouseEvent e) {
// this body is intentionally left empty
}
/**
* This method cannot be called directly.
*/
@Override
public void mouseExited(MouseEvent e) {
// this body is intentionally left empty
}
/**
* This method cannot be called directly.
*/
@Override
public void mousePressed(MouseEvent e) {
synchronized (mouseLock) {
mouseX = userX(e.getX());
mouseY = userY(e.getY());
isMousePressed = true;
}
if (e.getButton() == MouseEvent.BUTTON1) {
for (DrawListener listener : listeners)
listener.mousePressed(userX(e.getX()), userY(e.getY()));
}
}
/**
* This method cannot be called directly.
*/
@Override
public void mouseReleased(MouseEvent e) {
synchronized (mouseLock) {
isMousePressed = false;
}
if (e.getButton() == MouseEvent.BUTTON1) {
for (DrawListener listener : listeners)
listener.mouseReleased(userX(e.getX()), userY(e.getY()));
}
}
/**
* This method cannot be called directly.
*/
@Override
public void mouseDragged(MouseEvent e) {
synchronized (mouseLock) {
mouseX = userX(e.getX());
mouseY = userY(e.getY());
}
// doesn't seem to work if a button is specified
for (DrawListener listener : listeners)
listener.mouseDragged(userX(e.getX()), userY(e.getY()));
}
/**
* This method cannot be called directly.
*/
@Override
public void mouseMoved(MouseEvent e) {
synchronized (mouseLock) {
mouseX = userX(e.getX());
mouseY = userY(e.getY());
}
}
/***************************************************************************
* Keyboard interactions.
***************************************************************************/
/**
* Returns true if the user has typed a key.
*
* @return {@code true} if the user has typed a key; {@code false} otherwise
*/
public boolean hasNextKeyTyped() {
synchronized (keyLock) {
return !keysTyped.isEmpty();
}
}
/**
* The next key typed by the user.
*
* @return the next key typed by the user
*/
public char nextKeyTyped() {
synchronized (keyLock) {
return keysTyped.removeLast();
}
}
/**
* Returns true if the keycode is being pressed.
*
* This method takes as an argument the keycode (corresponding to a physical key).
* It can handle action keys (such as F1 and arrow keys) and modifier keys
* (such as shift and control).
* See {@link KeyEvent} for a description of key codes.
*
* @param keycode the keycode to check
* @return {@code true} if {@code keycode} is currently being pressed;
* {@code false} otherwise
*/
public boolean isKeyPressed(int keycode) {
synchronized (keyLock) {
return keysDown.contains(keycode);
}
}
/**
* This method cannot be called directly.
*/
@Override
public void keyTyped(KeyEvent e) {
synchronized (keyLock) {
keysTyped.addFirst(e.getKeyChar());
}
// notify all listeners
for (DrawListener listener : listeners)
listener.keyTyped(e.getKeyChar());
}
/**
* This method cannot be called directly.
*/
@Override
public void keyPressed(KeyEvent e) {
synchronized (keyLock) {
keysDown.add(e.getKeyCode());
}
// notify all listeners
for (DrawListener listener : listeners)
listener.keyPressed(e.getKeyCode());
}
/**
* This method cannot be called directly.
*/
@Override
public void keyReleased(KeyEvent e) {
synchronized (keyLock) {
keysDown.remove(e.getKeyCode());
}
// notify all listeners
for (DrawListener listener : listeners)
listener.keyPressed(e.getKeyCode());
}
/**
* Test client.
*
* @param args the command-line arguments
*/
public static void main(String[] args) {
// create one drawing window
Draw draw1 = new Draw("Test client 1");
draw1.square(0.2, 0.8, 0.1);
draw1.filledSquare(0.8, 0.8, 0.2);
draw1.circle(0.8, 0.2, 0.2);
draw1.setPenColor(Draw.MAGENTA);
draw1.setPenRadius(0.02);
draw1.arc(0.8, 0.2, 0.1, 200, 45);
// create another one
Draw draw2 = new Draw("Test client 2");
draw2.setCanvasSize(900, 200);
// draw a blue diamond
draw2.setPenRadius();
draw2.setPenColor(Draw.BLUE);
double[] x = { 0.1, 0.2, 0.3, 0.2 };
double[] y = { 0.2, 0.3, 0.2, 0.1 };
draw2.filledPolygon(x, y);
// text
draw2.setPenColor(Draw.BLACK);
draw2.text(0.2, 0.5, "bdfdfdfdlack text");
draw2.setPenColor(Draw.WHITE);
draw2.text(0.8, 0.8, "white text");
}
}
/******************************************************************************
* Copyright 2002-2018, Robert Sedgewick and Kevin Wayne.
*
* This file is part of algs4.jar, which accompanies the textbook
*
* Algorithms, 4th edition by Robert Sedgewick and Kevin Wayne,
* Addison-Wesley Professional, 2011, ISBN 0-321-57351-X.
* http://algs4.cs.princeton.edu
*
*
* algs4.jar is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* algs4.jar is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with algs4.jar. If not, see http://www.gnu.org/licenses.
******************************************************************************/