diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..82b2b9e --- /dev/null +++ b/.envrc @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# the shebang is ignored, but nice for editors + +if type -P lorri &>/dev/null; then + eval "$(lorri direnv)" +else + echo 'while direnv evaluated .envrc, could not find the command "lorri" [https://github.com/nix-community/lorri]' + use nix +fi diff --git a/common/Draw.java b/common/Draw.java new file mode 100644 index 0000000..e7495c6 --- /dev/null +++ b/common/Draw.java @@ -0,0 +1,1881 @@ +/****************************************************************************** + * 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 independent 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 common; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Component; +import java.awt.FileDialog; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.MediaTracker; +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.util.Timer; +import java.util.TimerTask; + +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.io.File; +import java.io.IOException; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URI; +import java.net.URISyntaxException; + +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; + +/** + * The Draw data type 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 independent 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 aqua (0, 255, 255). + */ + public static final Color AQUA = new Color(0, 255, 255); + + /** + * The color black (0, 0, 0). + */ + public static final Color BLACK = Color.BLACK; + + /** + * The color blue (0, 0, 255). + */ + public static final Color BLUE = Color.BLUE; + + /** + * The color cyan (0, 255, 255). + */ + public static final Color CYAN = Color.CYAN; + + /** + * The color fuscia (255, 0, 255). + */ + public static final Color FUSCIA = new Color(255, 0, 255); + + /** + * The color dark gray (64, 64, 64). + */ + public static final Color DARK_GRAY = Color.DARK_GRAY; + + /** + * The color gray (128, 128, 128). + */ + public static final Color GRAY = Color.GRAY; + + /** + * The color green (0, 128, 0). + */ + public static final Color GREEN = new Color(0, 128, 0); + + /** + * The color light gray (192, 192, 192). + */ + public static final Color LIGHT_GRAY = Color.LIGHT_GRAY; + + /** + * The color lime (0, 255, 0). + */ + public static final Color LIME = new Color(0, 255, 0); + + /** + * The color magenta (255, 0, 255). + */ + public static final Color MAGENTA = Color.MAGENTA; + + /** + * The color maroon (128, 0, 0). + */ + public static final Color MAROON = new Color(128, 0, 0); + + /** + * The color navy (0, 0, 128). + */ + public static final Color NAVY = new Color(0, 0, 128); + + /** + * The color olive (128, 128, 0). + */ + public static final Color OLIVE = new Color(128, 128, 0); + + /** + * The color orange (255, 200, 0). + */ + public static final Color ORANGE = Color.ORANGE; + + /** + * The color pink (255, 175, 175). + */ + public static final Color PINK = Color.PINK; + + /** + * The color purple (128, 0, 128). + */ + public static final Color PURPLE = new Color(128, 0, 128); + + /** + * The color red (255, 0, 0). + */ + public static final Color RED = Color.RED; + + /** + * The color silver (192, 192, 192). + */ + public static final Color SILVER = new Color(192, 192, 192); + + /** + * The color teal (0, 128, 128). + */ + public static final Color TEAL = new Color(0, 128, 128); + + /** + * The color white (255, 255, 255). + */ + public static final Color WHITE = Color.WHITE; + + /** + * The color yellow (255, 255, 0). + */ + public static final Color YELLOW = Color.YELLOW; + + /** + * A 100% transparent color, for a transparent background. + */ + public static final Color TRANSPARENT = new Color(0, 0, 0, 0); + + /** + * The 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); + + /** + * The 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); + + /** + * The 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); + + /** + * The shade of orange used in Princeton University'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_BACKGROUND_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); + + // default title of drawing window + private static final String DEFAULT_WINDOW_TITLE = "Draw"; + + // current pen color + private Color penColor = DEFAULT_PEN_COLOR; + + // background color + private Color backgroundColor = DEFAULT_BACKGROUND_COLOR; + + // current title of drawing window + private String windowTitle = DEFAULT_WINDOW_TITLE; + + // canvas size + private int width = DEFAULT_SIZE; + private int height = DEFAULT_SIZE; + + // current pen radius + private double penRadius = DEFAULT_PEN_RADIUS; + + // show we draw immediately or wait until next show? + private boolean defer = false; + + private double xmin = DEFAULT_XMIN; + private double xmax = DEFAULT_XMAX; + private double ymin = DEFAULT_YMIN; + private double ymax = DEFAULT_YMAX; + + // for synchronization + private final Object mouseLock = new Object(); + private final Object keyLock = new Object(); + + // current font + private Font font = DEFAULT_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; + + // is the JFrame visible (upon calling draw())? + private static boolean isJFrameVisible = true; + + // 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(); + + // timer + private Timer timer; + + /** + * Initializes an empty drawing object. + */ + public Draw() { + initCanvas(); + initGUI(); + } + + // initialize the drawing canvas + private void initCanvas() { + + // BufferedImage stuff + offscreenImage = new BufferedImage(2*width, 2*height, BufferedImage.TYPE_INT_ARGB); + onscreenImage = new BufferedImage(2*width, 2*height, BufferedImage.TYPE_INT_ARGB); + offscreen = offscreenImage.createGraphics(); + onscreen = onscreenImage.createGraphics(); + offscreen.scale(2.0, 2.0); // since we made it 2x as big + + // initialize drawing window + offscreen.setBackground(DEFAULT_BACKGROUND_COLOR); + offscreen.clearRect(0, 0, width, height); + onscreen.setBackground(DEFAULT_BACKGROUND_COLOR); + onscreen.clearRect(0, 0, 2*width, 2*height); + + // set the pen color + offscreen.setColor(penColor); + + // add antialiasing + RenderingHints hints = new RenderingHints(null); + hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + offscreen.addRenderingHints(hints); + } + + // initialize the GUI + private void initGUI() { + + // create the JFrame (if necessary) + if (frame == null) { + frame = new JFrame(); + frame.addKeyListener(this); // JLabel cannot get keyboard focus + frame.setFocusTraversalKeysEnabled(false); // allow VK_TAB with isKeyPressed() + frame.setResizable(false); + // frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // closes all windows + frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); // closes only current window + frame.setTitle(windowTitle); + frame.setJMenuBar(createMenuBar()); + } + + // create the ImageIcon + RetinaImageIcon icon = new RetinaImageIcon(onscreenImage); + draw = new JLabel(icon); + draw.addMouseListener(this); + draw.addMouseMotionListener(this); + + // finish up the JFrame + frame.setContentPane(draw); + frame.pack(); + frame.requestFocusInWindow(); + frame.setVisible(false); + } + + + /** + * Makes the drawing window visible or invisible. + * + * @param isVisible if {@code true}, makes the drawing window visible, + * otherwise hides the drawing window. + */ + public void setVisible(boolean isVisible) { + isJFrameVisible = isVisible; + frame.setVisible(isVisible); + } + + /** + * 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 512-by-512 pixels. + * This also clears the current drawing using the default background color (white). + * Ordinarily, this method is called once, at the very beginning of a program. + */ + public void setCanvasSize() { + setCanvasSize(DEFAULT_SIZE, DEFAULT_SIZE); + } + + /** + * Sets the canvas (drawing area) to be width-by-height pixels. + * This also clears the current drawing using the default background color (white). + * Ordinarily, 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; + initCanvas(); + initGUI(); + } + + + // create the menu bar + private JMenuBar createMenuBar() { + JMenuBar menuBar = new JMenuBar(); + JMenu menu = new JMenu("File"); + menuBar.add(menu); + JMenuItem menuItem1 = new JMenuItem(" Save... "); + menuItem1.addActionListener(this); + // Java 11: use getMenuShortcutKeyMaskEx() + // Java 8: use getMenuShortcutKeyMask() + menuItem1.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); + menu.add(menuItem1); + return menuBar; + } + + /** + * Closes the drawing window. + * This allows the client program to terminate instead of requiring + * the user to close the drawing window manually. + * Drawing after calling this method will restore the previous window state. + */ + public void close() { + frame.dispose(); + } + + /*************************************************************************** + * Input validation helper methods. + ***************************************************************************/ + + // throw an IllegalArgumentException if x is NaN or infinite + private static void validate(double x, String name) { + if (Double.isNaN(x)) throw new IllegalArgumentException(name + " is NaN"); + if (Double.isInfinite(x)) throw new IllegalArgumentException(name + " is infinite"); + } + + // throw an IllegalArgumentException if s is null + private static void validateNonnegative(double x, String name) { + if (x < 0) throw new IllegalArgumentException(name + " negative"); + } + + // throw an IllegalArgumentException if s is null + private static void validateNotNull(Object x, String name) { + if (x == null) throw new IllegalArgumentException(name + " is null"); + } + + + /*************************************************************************** + * Set the title of the drawing window. + ***************************************************************************/ + + /** + * Sets the title of the drawing window to the specified string. + * + * @param windowTitle the title of the window + * @throws IllegalArgumentException if {@code title} is {@code null} + */ + public void setTitle(String windowTitle) { + validateNotNull(windowTitle, "title"); + this.windowTitle = windowTitle; + frame.setTitle(windowTitle); + } + + /*************************************************************************** + * User and screen coordinate systems. + ***************************************************************************/ + + /** + * Sets the x-scale to the default range (between 0.0 and 1.0). + */ + public void setXscale() { + setXscale(DEFAULT_XMIN, DEFAULT_XMAX); + } + + /** + * Sets the y-scale to the default range (between 0.0 and 1.0). + */ + public void setYscale() { + setYscale(DEFAULT_YMIN, DEFAULT_YMAX); + } + + /** + * Sets the x-scale to the specified range. + * + * @param min the minimum value of the x-scale + * @param max the maximum value of the x-scale + * @throws IllegalArgumentException if {@code (max == min)} + * @throws IllegalArgumentException if either {@code min} or {@code max} is either NaN or infinite + */ + public void setXscale(double min, double max) { + validate(min, "min"); + validate(max, "max"); + double size = max - min; + if (size == 0.0) throw new IllegalArgumentException("the min and max are the same"); + xmin = min - BORDER * size; + xmax = max + BORDER * size; + } + + /** + * Sets the y-scale to the specified range. + * + * @param min the minimum value of the y-scale + * @param max the maximum value of the y-scale + * @throws IllegalArgumentException if {@code (max == min)} + * @throws IllegalArgumentException if either {@code min} or {@code max} is either NaN or infinite + */ + public void setYscale(double min, double max) { + validate(min, "min"); + validate(max, "max"); + double size = max - min; + if (size == 0.0) throw new IllegalArgumentException("the min and max are the same"); + ymin = min - BORDER * size; + ymax = max + BORDER * size; + } + + /** + * Sets both the x-scale and y-scale to the default range (between 0.0 and 1.0). + */ + public void setScale() { + setXscale(); + setYscale(); + } + + /** + * Sets both the x-scale and y-scale to the (same) specified range. + * @param min the minimum value of the y-scale + * @param max the maximum value of the y-scale + * @throws IllegalArgumentException if {@code (max == min)} + * @throws IllegalArgumentException if either {@code min} or {@code max} is either NaN or infinite + */ + public void setScale(double min, double max) { + setXscale(min, max); + setYscale(min, max); + } + + + // 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 using the default background color (white). + */ + public void clear() { + clear(DEFAULT_BACKGROUND_COLOR); + } + + /** + * Clears the screen using the specified background color. + * To make the background transparent, use {@code Draw.TRANSPARENT}. + * + * @param color the color to make the background + * @throws IllegalArgumentException if {@code color} is {@code null} + */ + public void clear(Color color) { + validateNotNull(color, "color"); + + backgroundColor = color; + offscreen.setBackground(backgroundColor); + offscreen.clearRect(0, 0, width, height); + + draw(); + } + + /** + * Returns the current pen radius. + * + * @return the current pen radius + */ + public double getPenRadius() { + return penRadius; + } + + /** + * Sets the pen radius to the default (0.002). + */ + public void setPenRadius() { + setPenRadius(DEFAULT_PEN_RADIUS); + } + + /** + * Sets the radius of the pen to the given size. + * + * @param radius the radius of the pen + * @throws IllegalArgumentException if {@code radius} is negative, NaN, or infinite + */ + public void setPenRadius(double radius) { + validate(radius, "pen radius"); + validateNonnegative(radius, "pen radius"); + + penRadius = radius * DEFAULT_SIZE; + BasicStroke stroke = new BasicStroke((float) penRadius, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); + // BasicStroke stroke = new BasicStroke((float) penRadius); + offscreen.setStroke(stroke); + } + + /** + * Returns the current pen color. + * + * @return the current pen color + */ + public Color getPenColor() { + return penColor; + } + + /** + * Returns the current background color. + * + * @return the current background color + */ + public Color getBackgroundColor() { + return backgroundColor; + } + + /** + * 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 + * @throws IllegalArgumentException if {@code color} is {@code null} + */ + public void setPenColor(Color color) { + validateNotNull(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 {@code red}, {@code green}, + * or {@code blue} is outside its prescribed range + */ + public void setPenColor(int red, int green, int blue) { + if (red < 0 || red >= 256) throw new IllegalArgumentException("red must be between 0 and 255"); + if (green < 0 || green >= 256) throw new IllegalArgumentException("green must be between 0 and 255"); + if (blue < 0 || blue >= 256) throw new IllegalArgumentException("blue must be between 0 and 255"); + setPenColor(new Color(red, green, blue)); + } + + + /** + * Turns on xor mode. + */ + public void xorOn() { + offscreen.setXORMode(backgroundColor); + } + + /** + * Turns off xor mode. + */ + public void xorOff() { + offscreen.setPaintMode(); + } + + /** + * Returns the current {@code JLabel} for use in some other GUI. + * + * @return the current {@code JLabel} + */ + public JLabel getJLabel() { + return draw; + } + + /** + * Returns 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 + * @throws IllegalArgumentException if {@code font} is {@code null} + */ + public void setFont(Font font) { + validateNotNull(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 + * @throws IllegalArgumentException if any coordinate is either NaN or infinite + */ + public void line(double x0, double y0, double x1, double y1) { + validate(x0, "x0"); + validate(y0, "y0"); + validate(x1, "x1"); + validate(y1, "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 + * @throws IllegalArgumentException if {@code x} or {@code y} is either NaN or infinite + */ + private void pixel(double x, double y) { + validate(x, "x"); + validate(y, "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 + * @throws IllegalArgumentException if either {@code x} or {@code y} is either NaN or infinite + */ + public void point(double x, double y) { + validate(x, "x"); + validate(y, "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 the specified radius, centered at (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 radius the radius of the circle + * @throws IllegalArgumentException if {@code radius} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public void circle(double x, double y, double radius) { + validate(x, "x"); + validate(y, "y"); + validate(radius, "radius"); + validateNonnegative(radius, "radius"); + + double xs = scaleX(x); + double ys = scaleY(y); + double ws = factorX(2*radius); + double hs = factorY(2*radius); + 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 the specified radius, centered at (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 radius the radius of the circle + * @throws IllegalArgumentException if {@code radius} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public void filledCircle(double x, double y, double radius) { + validate(x, "x"); + validate(y, "y"); + validate(radius, "radius"); + validateNonnegative(radius, "radius"); + + double xs = scaleX(x); + double ys = scaleY(y); + double ws = factorX(2*radius); + double hs = factorY(2*radius); + 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 the specified semimajor and semiminor axes, + * centered at (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 {@code semiMajorAxis} + * or {@code semiMinorAxis} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public void ellipse(double x, double y, double semiMajorAxis, double semiMinorAxis) { + validate(x, "x"); + validate(y, "y"); + validate(semiMajorAxis, "semimajor axis"); + validate(semiMinorAxis, "semiminor axis"); + validateNonnegative(semiMajorAxis, "semimajor axis"); + validateNonnegative(semiMinorAxis, "semiminor axis"); + + 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 a filled ellipse with the specified semimajor and semiminor axes, + * centered at (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 {@code semiMajorAxis} + * or {@code semiMinorAxis} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public void filledEllipse(double x, double y, double semiMajorAxis, double semiMinorAxis) { + validate(x, "x"); + validate(y, "y"); + validate(semiMajorAxis, "semimajor axis"); + validate(semiMinorAxis, "semiminor axis"); + validateNonnegative(semiMajorAxis, "semimajor axis"); + validateNonnegative(semiMinorAxis, "semiminor axis"); + + 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 a circular arc of the specified radius, + * centered at (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 radius 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 {@code radius} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public void arc(double x, double y, double radius, double angle1, double angle2) { + validate(x, "x"); + validate(y, "y"); + validate(radius, "arc radius"); + validate(angle1, "angle1"); + validate(angle2, "angle2"); + validateNonnegative(radius, "arc radius"); + + while (angle2 < angle1) angle2 += 360; + double xs = scaleX(x); + double ys = scaleY(y); + double ws = factorX(2*radius); + double hs = factorY(2*radius); + 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 the specified size, centered at (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 halfLength one half the length of any side of the square + * @throws IllegalArgumentException if {@code halfLength} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public void square(double x, double y, double halfLength) { + validate(x, "x"); + validate(y, "y"); + validate(halfLength, "halfLength"); + validateNonnegative(halfLength, "half length"); + + double xs = scaleX(x); + double ys = scaleY(y); + double ws = factorX(2*halfLength); + double hs = factorY(2*halfLength); + 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 square of the specified size, centered at (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 halfLength one half the length of any side of the square + * @throws IllegalArgumentException if {@code halfLength} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public void filledSquare(double x, double y, double halfLength) { + validate(x, "x"); + validate(y, "y"); + validate(halfLength, "halfLength"); + validateNonnegative(halfLength, "half length"); + + double xs = scaleX(x); + double ys = scaleY(y); + double ws = factorX(2*halfLength); + double hs = factorY(2*halfLength); + 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 the specified size, centered at (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 one half the width of the rectangle + * @param halfHeight one half the height of the rectangle + * @throws IllegalArgumentException if either {@code halfWidth} or {@code halfHeight} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public void rectangle(double x, double y, double halfWidth, double halfHeight) { + validate(x, "x"); + validate(y, "y"); + validate(halfWidth, "halfWidth"); + validate(halfHeight, "halfHeight"); + validateNonnegative(halfWidth, "half width"); + validateNonnegative(halfHeight, "half height"); + + 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 the specified size, centered at (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 one half the width of the rectangle + * @param halfHeight one half the height of the rectangle + * @throws IllegalArgumentException if either {@code halfWidth} or {@code halfHeight} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public void filledRectangle(double x, double y, double halfWidth, double halfHeight) { + validate(x, "x"); + validate(y, "y"); + validate(halfWidth, "halfWidth"); + validate(halfHeight, "halfHeight"); + validateNonnegative(halfWidth, "half width"); + validateNonnegative(halfHeight, "half height"); + + 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 vertices + * (x0, y0), + * (x1, y1), ..., + * (xn–1, yn–1). + * + * @param x an array of all the x-coordinates of the polygon + * @param y an array of all the y-coordinates of the polygon + * @throws IllegalArgumentException unless {@code x[]} and {@code y[]} + * are of the same length + * @throws IllegalArgumentException if any coordinate is either NaN or infinite + * @throws IllegalArgumentException if either {@code x[]} or {@code y[]} is {@code null} + */ + public void polygon(double[] x, double[] y) { + validateNotNull(x, "x-coordinate array"); + validateNotNull(y, "y-coordinate array"); + for (int i = 0; i < x.length; i++) validate(x[i], "x[" + i + "]"); + for (int i = 0; i < y.length; i++) validate(y[i], "y[" + i + "]"); + + int n1 = x.length; + int n2 = y.length; + if (n1 != n2) throw new IllegalArgumentException("arrays must be of the same length"); + int n = n1; + if (n == 0) return; + + 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 vertices + * (x0, y0), + * (x1, y1), ..., + * (xn–1, yn–1). + * + * @param x an array of all the x-coordinates of the polygon + * @param y an array of all the y-coordinates of the polygon + * @throws IllegalArgumentException unless {@code x[]} and {@code y[]} + * are of the same length + * @throws IllegalArgumentException if any coordinate is either NaN or infinite + * @throws IllegalArgumentException if either {@code x[]} or {@code y[]} is {@code null} + */ + public void filledPolygon(double[] x, double[] y) { + validateNotNull(x, "x-coordinate array"); + validateNotNull(y, "y-coordinate array"); + for (int i = 0; i < x.length; i++) validate(x[i], "x[" + i + "]"); + for (int i = 0; i < y.length; i++) validate(y[i], "y[" + i + "]"); + + int n1 = x.length; + int n2 = y.length; + if (n1 != n2) throw new IllegalArgumentException("arrays must be of the same length"); + int n = n1; + if (n == 0) return; + + 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. + ***************************************************************************/ + + // get an image from the given filename + private static Image getImage(String filename) { + if (filename == null) throw new IllegalArgumentException(); + + // to read from file + ImageIcon icon = new ImageIcon(filename); + + // try to read from URL + if (icon.getImageLoadStatus() != MediaTracker.COMPLETE) { + try { + URI uri = new URI(filename); + if (uri.isAbsolute()) { + URL url = uri.toURL(); + icon = new ImageIcon(url); + } + } + catch (MalformedURLException | URISyntaxException e) { + /* not a url */ + } + } + + // in case file is inside a .jar (classpath relative to Draw) + if (icon.getImageLoadStatus() != MediaTracker.COMPLETE) { + URL url = Draw.class.getResource(filename); + if (url != null) + icon = new ImageIcon(url); + } + + // in case file is inside a .jar (classpath relative to root of jar) + if (icon.getImageLoadStatus() != MediaTracker.COMPLETE) { + URL url = Draw.class.getResource("/" + filename); + if (url == null) throw new IllegalArgumentException("could not read image: '" + filename + "'"); + icon = new ImageIcon(url); + } + + return icon.getImage(); + } + + /** + * Draws the specified image centered at (x, y). + * The supported image formats are typically JPEG, PNG, GIF, TIFF, and BMP. + * As an optimization, the picture is cached, so there is no performance + * penalty for redrawing the same image multiple times (e.g., in an animation). + * However, if you change the picture file after drawing it, subsequent + * calls will draw the original picture. + * + * @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 filename is invalid + * @throws IllegalArgumentException if either {@code x} or {@code y} is either NaN or infinite + */ + public void picture(double x, double y, String filename) { + validate(x, "x"); + validate(y, "y"); + validateNotNull(filename, "filename"); + + Image image = getImage(filename); + 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 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 the specified image centered at (x, y), + * rotated given number of degrees. + * The supported image formats are typically JPEG, PNG, GIF, TIFF, and BMP. + * + * @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 filename is invalid + * @throws IllegalArgumentException if {@code x}, {@code y}, {@code degrees} is NaN or infinite + * @throws IllegalArgumentException if {@code filename} is {@code null} + */ + public void picture(double x, double y, String filename, double degrees) { + validate(x, "x"); + validate(y, "y"); + validate(degrees, "degrees"); + validateNotNull(filename, "filename"); + + Image image = getImage(filename); + 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 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 the specified image centered at (x, y), + * rescaled to the specified bounding box. + * The supported image formats are typically JPEG, PNG, GIF, TIFF, and BMP. + * + * @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 scaledWidth the width of the scaled image (in screen coordinates) + * @param scaledHeight the height of the scaled image (in screen coordinates) + * @throws IllegalArgumentException if either {@code scaledWidth} + * or {@code scaledHeight} is negative + * @throws IllegalArgumentException if the image filename is invalid + * @throws IllegalArgumentException if {@code x} or {@code y} is either NaN or infinite + * @throws IllegalArgumentException if {@code filename} is {@code null} + */ + public void picture(double x, double y, String filename, double scaledWidth, double scaledHeight) { + validate(x, "x"); + validate(y, "y"); + validate(scaledWidth, "scaled width"); + validate(scaledHeight, "scaled height"); + validateNotNull(filename, "filename"); + validateNonnegative(scaledWidth, "scaled width"); + validateNonnegative(scaledHeight, "scaled height"); + + Image image = getImage(filename); + double xs = scaleX(x); + double ys = scaleY(y); + double ws = factorX(scaledWidth); + double hs = factorY(scaledHeight); + 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 the specified image centered at (x, y), rotated + * given number of degrees, and rescaled to the specified bounding box. + * The supported image formats are typically JPEG, PNG, GIF, TIFF, and BMP. + * + * @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 scaledWidth the width of the scaled image (in screen coordinates) + * @param scaledHeight the height of the scaled image (in screen coordinates) + * @param degrees is the number of degrees to rotate counterclockwise + * @throws IllegalArgumentException if either {@code scaledWidth} + * or {@code scaledHeight} is negative + * @throws IllegalArgumentException if the image filename is invalid + */ + public void picture(double x, double y, String filename, double scaledWidth, double scaledHeight, double degrees) { + validate(x, "x"); + validate(y, "y"); + validate(scaledWidth, "scaled width"); + validate(scaledHeight, "scaled height"); + validate(degrees, "degrees"); + validateNotNull(filename, "filename"); + validateNonnegative(scaledWidth, "scaled width"); + validateNonnegative(scaledHeight, "scaled height"); + + Image image = getImage(filename); + double xs = scaleX(x); + double ys = scaleY(y); + double ws = factorX(scaledWidth); + double hs = factorY(scaledHeight); + 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 at (x, y). + * + * @param x the center x-coordinate of the text + * @param y the center y-coordinate of the text + * @param text the text to write + * @throws IllegalArgumentException if {@code text} is {@code null} + * @throws IllegalArgumentException if {@code x} or {@code y} is either NaN or infinite + */ + public void text(double x, double y, String text) { + validate(x, "x"); + validate(y, "y"); + validateNotNull(text, "text"); + + offscreen.setFont(font); + FontMetrics metrics = offscreen.getFontMetrics(); + double xs = scaleX(x); + double ys = scaleY(y); + int ws = metrics.stringWidth(text); + int hs = metrics.getDescent(); + offscreen.drawString(text, (float) (xs - ws/2.0), (float) (ys + hs)); + draw(); + } + + /** + * Writes the given text string in the current font, centered at (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 text the text to write + * @param degrees is the number of degrees to rotate counterclockwise + * @throws IllegalArgumentException if {@code text} is {@code null} + * @throws IllegalArgumentException if {@code x}, {@code y}, or {@code degrees} is either NaN or infinite + */ + public void text(double x, double y, String text, double degrees) { + validate(x, "x"); + validate(y, "y"); + validate(degrees, "degrees"); + validateNotNull(text, "text"); + + double xs = scaleX(x); + double ys = scaleY(y); + offscreen.rotate(Math.toRadians(-degrees), xs, ys); + text(x, y, text); + 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 text the text + * @throws IllegalArgumentException if {@code text} is {@code null} + * @throws IllegalArgumentException if {@code x} or {@code y} is either NaN or infinite + */ + public void textLeft(double x, double y, String text) { + validate(x, "x"); + validate(y, "y"); + validateNotNull(text, "text"); + + offscreen.setFont(font); + FontMetrics metrics = offscreen.getFontMetrics(); + double xs = scaleX(x); + double ys = scaleY(y); + // int ws = metrics.stringWidth(text); + int hs = metrics.getDescent(); + offscreen.drawString(text, (float) xs, (float) (ys + hs)); + draw(); + } + + /** + * Writes the given text string in the current font, right-aligned at (x, y). + * + * @param x the x-coordinate of the text + * @param y the y-coordinate of the text + * @param text the text to write + * @throws IllegalArgumentException if {@code text} is {@code null} + * @throws IllegalArgumentException if {@code x} or {@code y} is either NaN or infinite + */ + public void textRight(double x, double y, String text) { + validate(x, "x"); + validate(y, "y"); + validateNotNull(text, "text"); + + offscreen.setFont(font); + FontMetrics metrics = offscreen.getFontMetrics(); + double xs = scaleX(x); + double ys = scaleY(y); + int ws = metrics.stringWidth(text); + int hs = metrics.getDescent(); + offscreen.drawString(text, (float) (xs - ws), (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 + * @throws IllegalArgumentException if {@code t} is negative + * @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 + * @throws IllegalArgumentException if {@code t} is negative + */ + 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.setBackground(backgroundColor); + onscreen.clearRect(0, 0, 2*width, 2*height); + onscreen.drawImage(offscreenImage, 0, 0, null); + + // make frame visible upon first call to show() + if (frame.isVisible() != isJFrameVisible) { + frame.setVisible(isJFrameVisible); + } + + 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 deferred 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 the drawing to a file in a supported file format + * (typically JPEG, PNG, GIF, TIFF, and BMP). + * The filetype extension must be {@code .jpg}, {@code .png}, {@code .gif}, + * {@code .bmp}, or {@code .tif}. + * + * @param filename the name of the file + * @throws IllegalArgumentException if {@code filename} is {@code null} + * @throws IllegalArgumentException if {@code filename} is the empty string + * @throws IllegalArgumentException if {@code filename} has invalid filetype extension + * @throws IllegalArgumentException if cannot write the file {@code filename} + */ + public void save(String filename) { + validateNotNull(filename, "filename"); + if (filename.length() == 0) { + throw new IllegalArgumentException("argument to save() is the empty string"); + } + + File file = new File(filename); + String suffix = filename.substring(filename.lastIndexOf('.') + 1); + if (!filename.contains(".") || suffix.length() == 0) { + throw new IllegalArgumentException("the filename '" + filename + "' has no file extension, such as .jpg or .png"); + } + + try { + // if the file format supports transparency (such as PNG or GIF) + if (ImageIO.write(onscreenImage, suffix, file)) return; + + // if the file format does not support transparency (such as JPEG or BMP) + BufferedImage saveImage = new BufferedImage(2*width, 2*height, BufferedImage.TYPE_INT_RGB); + saveImage.createGraphics().drawImage(onscreenImage, 0, 0, Color.WHITE, null); + if (ImageIO.write(saveImage, suffix, file)) return; + + // failed to save the file; probably wrong format + throw new IllegalArgumentException("the filetype '" + suffix + "' is not supported"); + } + catch (IOException e) { + throw new IllegalArgumentException("could not write the file + " + filename, e); + } + } + + /** + * This method cannot be called directly. + */ + @Override + public void actionPerformed(ActionEvent event) { + FileDialog chooser = new FileDialog(frame, "Use a .png or .jpg extension", FileDialog.SAVE); + chooser.setVisible(true); + String selectedDirectory = chooser.getDirectory(); + String selectedFilename = chooser.getFile(); + if (selectedDirectory != null && selectedFilename != null) { + try { + save(selectedDirectory + selectedFilename); + } + catch (IllegalArgumentException e) { + System.err.println(e.getMessage()); + } + } + } + + + + /*************************************************************************** + * 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 listening to events + show(); + listeners.add(listener); + } + + + + + /*************************************************************************** + * 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 mouseEntered(MouseEvent event) { + // this body is intentionally left empty + } + + /** + * This method cannot be called directly. + */ + @Override + public void mouseExited(MouseEvent event) { + // this body is intentionally left empty + } + + /** + * This method cannot be called directly. + */ + @Override + public void mousePressed(MouseEvent event) { + synchronized (mouseLock) { + mouseX = userX(event.getX()); + mouseY = userY(event.getY()); + isMousePressed = true; + } + if (event.getButton() == MouseEvent.BUTTON1) { + for (DrawListener listener : listeners) + listener.mousePressed(userX(event.getX()), userY(event.getY())); + } + + } + + /** + * This method cannot be called directly. + */ + @Override + public void mouseReleased(MouseEvent event) { + synchronized (mouseLock) { + isMousePressed = false; + } + if (event.getButton() == MouseEvent.BUTTON1) { + for (DrawListener listener : listeners) + listener.mouseReleased(userX(event.getX()), userY(event.getY())); + } + } + + /** + * This method cannot be called directly. + */ + @Override + public void mouseClicked(MouseEvent event) { + if (event.getButton() == MouseEvent.BUTTON1) { + for (DrawListener listener : listeners) + listener.mouseClicked(userX(event.getX()), userY(event.getY())); + } + } + + + /** + * This method cannot be called directly. + */ + @Override + public void mouseDragged(MouseEvent event) { + synchronized (mouseLock) { + mouseX = userX(event.getX()); + mouseY = userY(event.getY()); + } + // doesn't seem to work if a button is specified + for (DrawListener listener : listeners) + listener.mouseDragged(userX(event.getX()), userY(event.getY())); + } + + /** + * This method cannot be called directly. + */ + @Override + public void mouseMoved(MouseEvent event) { + synchronized (mouseLock) { + mouseX = userX(event.getX()); + mouseY = userY(event.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 event) { + synchronized (keyLock) { + keysTyped.addFirst(event.getKeyChar()); + } + + // notify all listeners + for (DrawListener listener : listeners) + listener.keyTyped(event.getKeyChar()); + } + + /** + * This method cannot be called directly. + */ + @Override + public void keyPressed(KeyEvent event) { + synchronized (keyLock) { + keysDown.add(event.getKeyCode()); + } + + // notify all listeners + for (DrawListener listener : listeners) + listener.keyPressed(event.getKeyCode()); + } + + /** + * This method cannot be called directly. + */ + @Override + public void keyReleased(KeyEvent event) { + synchronized (keyLock) { + keysDown.remove(event.getKeyCode()); + } + + // notify all listeners + for (DrawListener listener : listeners) + listener.keyReleased(event.getKeyCode()); + } + + /*************************************************************************** + * Timer events. + ***************************************************************************/ + + /** + * Sets a timer that calls update() method a specified number of times + * per second. + *

+ * @param callsPerSecond calls per second + */ + public void enableTimer(int callsPerSecond) { + disableTimer(); + timer = new Timer(); + timer.schedule(new MyTimerTask(), 0, (int) Math.round(1000.0 / callsPerSecond)); + } + + public void disableTimer() { + if (timer != null) timer.cancel(); + } + + private class MyTimerTask extends TimerTask { + public void run() { + for (DrawListener listener : listeners) + listener.update(); + } + } + + /*************************************************************************** + * For improved resolution on Mac Retina displays. + ***************************************************************************/ + + private static class RetinaImageIcon extends ImageIcon { + + public RetinaImageIcon(Image image) { + super(image); + } + + public int getIconWidth() { + return super.getIconWidth() / 2; + } + + /** + * Returns the height of the icon. + * + * @return the height in pixels of this icon + */ + public int getIconHeight() { + return super.getIconHeight() / 2; + } + + public synchronized void paintIcon(Component c, Graphics g, int x, int y) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.scale(0.5, 0.5); + super.paintIcon(c, g2, x * 2, y * 2); + g2.dispose(); + } + } + + /** + * Test client. + * + * @param args the command-line arguments + */ + public static void main(String[] args) { + + // create one drawing window + Draw draw1 = new Draw(); + draw1.setTitle("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(); + draw2.setCanvasSize(900, 200); + draw2.setTitle("Test client 2"); + // 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, "black text"); + draw2.setPenColor(Draw.WHITE); + draw2.text(0.2, 0.2, "white text"); + } + +} diff --git a/common/StdDraw.java b/common/StdDraw.java new file mode 100644 index 0000000..bde2c22 --- /dev/null +++ b/common/StdDraw.java @@ -0,0 +1,2260 @@ +/****************************************************************************** + * Compilation: javac StdDraw.java + * Execution: java StdDraw + * Dependencies: none + * + * Standard 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 geometric shapes (e.g., + * points, lines, circles, rectangles) in a window on your computer + * and to save the drawings to a file. + * + * Todo + * ---- + * - Don't show window until first unbuffered drawing command or call to show() + * (with setVisible not set to false). + * - Add support for gradient fill, etc. + * - Fix setCanvasSize() so that it can be called only once. + * - On some systems, drawing a line (or other shape) that extends way + * beyond canvas (e.g., to infinity) dimensions does not get drawn. + * + * Remarks + * ------- + * - don't use AffineTransform for rescaling since it inverts + * images and strings + * + ******************************************************************************/ +package common; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Component; +import java.awt.FileDialog; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.MediaTracker; +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.io.File; +import java.io.IOException; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URI; +import java.net.URISyntaxException; + +import java.util.LinkedList; +import java.util.TreeSet; +import java.util.NoSuchElementException; +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; + +/** + * The {@code StdDraw} class provides static methods for creating drawings + * with your programs. It uses a simple graphics model that + * allows you to create drawings consisting of points, lines, squares, + * circles, and other geometric shapes in a window on your computer and + * to save the drawings to a file. Standard drawing also includes + * facilities for text, color, pictures, and animation, along with + * user interaction via the keyboard and mouse. + *

+ * Getting started. + * To use this class, you must have {@code StdDraw.class} in your + * Java classpath. If you used our autoinstaller, you should be all set. + * Otherwise, either download + * stdlib.jar + * and add to your Java classpath or download + * StdDraw.java + * and put a copy in your working directory. + *

+ * Now, cut-and-paste the following short program into your editor: + *

+ *   public class TestStdDraw {
+ *       public static void main(String[] args) {
+ *           StdDraw.setPenRadius(0.05);
+ *           StdDraw.setPenColor(StdDraw.BLUE);
+ *           StdDraw.point(0.5, 0.5);
+ *           StdDraw.setPenColor(StdDraw.MAGENTA);
+ *           StdDraw.line(0.2, 0.2, 0.8, 0.2);
+ *       }
+ *   }
+ *  
+ * If you compile and execute the program, you should see a window + * appear with a thick magenta line and a blue point. + * This program illustrates the two main types of methods in standard + * drawing—methods that draw geometric shapes and methods that + * control drawing parameters. + * The methods {@code StdDraw.line()} and {@code StdDraw.point()} + * draw lines and points; the methods {@code StdDraw.setPenRadius()} + * and {@code StdDraw.setPenColor()} control the line thickness and color. + *

+ * Points and lines. + * You can draw points and line segments with the following methods: + *

+ *

+ * The x- and y-coordinates must be in the drawing area + * (between 0 and 1 and by default) or the points and lines will not be visible. + *

+ * Squares, circles, rectangles, and ellipses. + * You can draw squares, circles, rectangles, and ellipses using + * the following methods: + *

+ *

+ * All of these methods take as arguments the location and size of the shape. + * The location is always specified by the x- and y-coordinates + * of its center. + * The size of a circle is specified by its radius and the size of an ellipse is + * specified by the lengths of its semi-major and semi-minor axes. + * The size of a square or rectangle is specified by its half-width or half-height. + * The convention for drawing squares and rectangles is parallel to those for + * drawing circles and ellipses, but may be unexpected to the uninitiated. + *

+ * The methods above trace outlines of the given shapes. The following methods + * draw filled versions: + *

+ *

+ * Circular arcs. + * You can draw circular arcs with the following method: + *

+ *

+ * The arc is from the circle centered at (x, y) of the specified radius. + * The arc extends from angle1 to angle2. By convention, the angles are + * polar (counterclockwise angle from the x-axis) + * and represented in degrees. For example, {@code StdDraw.arc(0.0, 0.0, 1.0, 0, 90)} + * draws the arc of the unit circle from 3 o'clock (0 degrees) to 12 o'clock (90 degrees). + *

+ * Polygons. + * You can draw polygons with the following methods: + *

+ *

+ * The points in the polygon are ({@code x[i]}, {@code y[i]}). + * For example, the following code fragment draws a filled diamond + * with vertices (0.1, 0.2), (0.2, 0.3), (0.3, 0.2), and (0.2, 0.1): + *

+ *   double[] x = { 0.1, 0.2, 0.3, 0.2 };
+ *   double[] y = { 0.2, 0.3, 0.2, 0.1 };
+ *   StdDraw.filledPolygon(x, y);
+ *  
+ *

+ * Pen size. + * The pen is circular, so that when you set the pen radius to r + * and draw a point, you get a circle of radius r. Also, lines are + * of thickness 2r and have rounded ends. The default pen radius + * is 0.002 and is not affected by coordinate scaling. This default pen + * radius is about 1/500 the width of the default canvas, so that if + * you draw 200 points equally spaced along a horizontal or vertical line, + * you will be able to see individual circles, but if you draw 250 such + * points, the result will look like a line. + *

+ *

+ * For example, {@code StdDraw.setPenRadius(0.01)} makes + * the thickness of the lines and the size of the points to be five times + * the 0.002 default. + * To draw points with the minimum possible radius (one pixel on typical + * displays), set the pen radius to 0.0. + *

+ * Pen color. + * All geometric shapes (such as points, lines, and circles) are drawn using + * the current pen color. By default, it is black. + * You can change the pen color with the following methods: + *

+ *

+ * The first method allows you to specify colors using the RGB color system. + * This color picker + * is a convenient way to find a desired color. + *

+ * The second method allows you to specify colors using the + * {@link Color} data type, which is defined in Java's {@link java.awt} package. + * Standard drawing defines a number of predefined colors including + * {@link #BLACK}, {@link #WHITE}, {@link #RED}, {@link #GREEN}, + * and {@link #BLUE}. + * For example, {@code StdDraw.setPenColor(StdDraw.RED)} sets the + * pen color to red. + *

+ * Window title. + * By default, the standard drawing window title is "Standard Draw". + * You can change the title with the following method: + *

+ *

+ * This sets the standard drawing window title to the specified string. + *

+ * Canvas size. + * By default, all drawing takes places in a 512-by-512 canvas. + * The canvas does not include the window title or window border. + * You can change the size of the canvas with the following method: + *

+ *

+ * This sets the canvas size to be width-by-height pixels. + * It also clears the current drawing using the default background color (white). + * Ordinarily, this method is called only once, at the very beginning of a program. + * For example, {@code StdDraw.setCanvasSize(800, 800)} + * sets the canvas size to be 800-by-800 pixels. + *

+ * Canvas scale and coordinate system. + * By default, all drawing takes places in the unit square, with (0, 0) at + * lower left and (1, 1) at upper right. You can change the default + * coordinate system with the following methods: + *

+ *

+ * The arguments are the coordinates of the minimum and maximum + * x- or y-coordinates that will appear in the canvas. + * For example, if you wish to use the default coordinate system but + * leave a small margin, you can call {@code StdDraw.setScale(-.05, 1.05)}. + *

+ * These methods change the coordinate system for subsequent drawing + * commands; they do not affect previous drawings. + * These methods do not change the canvas size; so, if the x- + * and y-scales are different, squares will become rectangles + * and circles will become ellipses. + *

+ * Text. + * You can use the following methods to annotate your drawings with text: + *

+ *

+ * The first two methods write the specified text in the current font, + * centered at (x, y). + * The second method allows you to rotate the text. + * The last two methods either left- or right-align the text at (x, y). + *

+ * The default font is a Sans Serif font with point size 16. + * You can use the following method to change the font: + *

+ *

+ * To specify the font, you use the {@link Font} data type, + * which is defined in Java's {@link java.awt} package. + * This allows you to + * choose the face, size, and style of the font. For example, the following + * code fragment sets the font to Arial Bold, 60 point. + * The import statement allows you to refer to Font + * directly, without needing the fully qualified name java.awt.Font. + *

+ *   import java.awt.Font;
+ *   ...
+ *   Font font = new Font("Arial", Font.BOLD, 60);
+ *   StdDraw.setFont(font);
+ *   StdDraw.text(0.5, 0.5, "Hello, World");
+ *  
+ *

+ * Images. + * You can use the following methods to add images to your drawings: + *

+ *

+ * These methods draw the specified image, centered at (x, y). + * The image must be in a supported file format (typically JPEG, PNG, GIF, TIFF, and BMP). + * The image will display at its native size, independent of the coordinate system. + * Optionally, you can rotate the image a specified number of degrees counterclockwise + * or rescale it to fit snugly inside a bounding box. + *

+ * Saving to a file. + * You can save your image to a file using the File → Save menu option. + * You can also save a file programmatically using the following method: + *

+ *

+ * You can save the drawing to a file in a supported file format + * (typically JPEG, PNG, GIF, TIFF, and BMP). + * + *

File formats. + * The {@code StdDraw} class supports reading and writing images to any of the + * file formats supported by {@link javax.imageio} (typically JPEG, PNG, + * GIF, TIFF, and BMP). + * The file extensions corresponding to JPEG, PNG, GIF, TIFF, and BMP, + * are {@code .jpg}, {@code .png}, {@code .gif}, {@code .tif}, + * and {@code .bmp}, respectively. + *

+ * We recommend using PNG for drawing that consist solely of geometric shapes + * and JPEG for drawings that contains pictures. + * The JPEG file format does not support transparent backgrounds. + * + *

+ * Clearing the canvas. + * To clear the entire drawing canvas, you can use the following methods: + *

+ *

+ * The first method clears the canvas to the default background color (white); + * the second method allows you to specify the background color. For example, + * {@code StdDraw.clear(StdDraw.LIGHT_GRAY)} clears the canvas to a shade + * of gray. To make the background transparent, + * call {@code StdDraw.clear(StdDraw.TRANSPARENT)}. + * + *

+ * Computer animations and double buffering. + * Double buffering is one of the most powerful features of standard drawing, + * enabling computer animations. + * The following methods control the way in which objects are drawn: + *

+ *

+ * By default, double buffering is disabled, which means that as soon as you + * call a drawing + * method—such as {@code point()} or {@code line()}—the + * results appear on the screen. + *

+ * When double buffering is enabled by calling {@link #enableDoubleBuffering()}, + * all drawing takes place on the offscreen canvas. The offscreen canvas + * is not displayed. Only when you call + * {@link #show()} does your drawing get copied from the offscreen canvas to + * the onscreen canvas, where it is displayed in the standard drawing window. You + * can think of double buffering as collecting all of the lines, points, shapes, + * and text that you tell it to draw, and then drawing them all + * simultaneously, upon request. + *

+ * The most important use of double buffering is to produce computer + * animations, creating the illusion of motion by rapidly + * displaying static drawings. To produce an animation, repeat + * the following four steps: + *

+ *

+ * The {@link #clear()}, {@link #show()}, and {@link #pause(int t)} methods + * support the first, third, and fourth of these steps, respectively. + *

+ * For example, this code fragment animates two balls moving in a circle. + *

+ *   StdDraw.setScale(-2.0, +2.0);
+ *   StdDraw.enableDoubleBuffering();
+ *
+ *   for (double t = 0.0; true; t += 0.02) {
+ *       double x = Math.sin(t);
+ *       double y = Math.cos(t);
+ *       StdDraw.clear();
+ *       StdDraw.filledCircle(x, y, 0.1);
+ *       StdDraw.filledCircle(-x, -y, 0.1);
+ *       StdDraw.show();
+ *       StdDraw.pause(20);
+ *   }
+ *  
+ * Without double buffering, the balls would flicker as they move. + *

+ * Keyboard and mouse inputs. + * Standard drawing has very basic support for keyboard and mouse input. + * It is much less powerful than most user interface libraries provide, but also much simpler. + * You can use the following methods to intercept mouse events: + *

+ *

+ * The first method tells you whether a mouse button is currently being pressed. + * The last two methods tells you the x- and y-coordinates of the mouse's + * current position, using the same coordinate system as the canvas (the unit square, by default). + * You should use these methods in an animation loop that waits a short while before trying + * to poll the mouse for its current state. + * You can use the following methods to intercept keyboard events: + *

+ *

+ * If the user types lots of keys, they will be saved in a list until you process them. + * The first method tells you whether the user has typed a key (that your program has + * not yet processed). + * The second method returns the next key that the user typed (that your program has + * not yet processed) and removes it from the list of saved keystrokes. + * The third method tells you whether a key is currently being pressed. + *

+ * Accessing control parameters. + * You can use the following methods to access the current pen color, pen radius, + * and font: + *

+ *

+ * These methods are useful when you want to temporarily change a + * control parameter and, later, reset it back to its original value. + *

+ * Corner cases. + * Here are some corner cases. + *

+ *

+ * Performance tricks. + * Standard drawing is capable of drawing large amounts of data. + * Here are a few tricks and tips: + *

+ *

+ * Known bugs and issues. + *

+ *

+ * Reference. + * For additional documentation, + * see Section 1.5 of + * Computer Science: An Interdisciplinary Approach + * by Robert Sedgewick and Kevin Wayne. + * + * @author Robert Sedgewick + * @author Kevin Wayne + */ +public final class StdDraw implements ActionListener, MouseListener, MouseMotionListener, KeyListener { + + /** + * The color aqua (0, 255, 255). + */ + public static final Color AQUA = new Color(0, 255, 255); + + /** + * The color black (0, 0, 0). + */ + public static final Color BLACK = Color.BLACK; + + /** + * The color blue (0, 0, 255). + */ + public static final Color BLUE = Color.BLUE; + + /** + * The color cyan (0, 255, 255). + */ + public static final Color CYAN = Color.CYAN; + + /** + * The color fuscia (255, 0, 255). + */ + public static final Color FUSCIA = new Color(255, 0, 255); + + /** + * The color dark gray (64, 64, 64). + */ + public static final Color DARK_GRAY = Color.DARK_GRAY; + + /** + * The color gray (128, 128, 128). + */ + public static final Color GRAY = Color.GRAY; + + /** + * The color green (0, 128, 0). + */ + public static final Color GREEN = new Color(0, 128, 0); + + /** + * The color light gray (192, 192, 192). + */ + public static final Color LIGHT_GRAY = Color.LIGHT_GRAY; + + /** + * The color lime (0, 255, 0). + */ + public static final Color LIME = new Color(0, 255, 0); + + /** + * The color magenta (255, 0, 255). + */ + public static final Color MAGENTA = Color.MAGENTA; + + /** + * The color maroon (128, 0, 0). + */ + public static final Color MAROON = new Color(128, 0, 0); + + /** + * The color navy (0, 0, 128). + */ + public static final Color NAVY = new Color(0, 0, 128); + + /** + * The color olive (128, 128, 0). + */ + public static final Color OLIVE = new Color(128, 128, 0); + + /** + * The color orange (255, 200, 0). + */ + public static final Color ORANGE = Color.ORANGE; + + /** + * The color pink (255, 175, 175). + */ + public static final Color PINK = Color.PINK; + + /** + * The color purple (128, 0, 128). + */ + public static final Color PURPLE = new Color(128, 0, 128); + + /** + * The color red (255, 0, 0). + */ + public static final Color RED = Color.RED; + + /** + * The color silver (192, 192, 192). + */ + public static final Color SILVER = new Color(192, 192, 192); + + /** + * The color teal (0, 128, 128). + */ + public static final Color TEAL = new Color(0, 128, 128); + + /** + * The color white (255, 255, 255). + */ + public static final Color WHITE = Color.WHITE; + + /** + * The color yellow (255, 255, 0). + */ + public static final Color YELLOW = Color.YELLOW; + + /** + * A 100% transparent color, for a transparent background. + */ + public static final Color TRANSPARENT = new Color(0, 0, 0, 0); + + /** + * The 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); + + /** + * The 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); + + /** + * The 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); + + /** + * The shade of orange used in Princeton University'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_BACKGROUND_COLOR = WHITE; + + // current pen color + private static Color penColor = DEFAULT_PEN_COLOR; + + // current background color + private static Color backgroundColor = DEFAULT_BACKGROUND_COLOR; + + // default title of standard drawing window + private static final String DEFAULT_WINDOW_TITLE = "Standard Draw"; + + // current title of standard drawing window + private static String windowTitle = DEFAULT_WINDOW_TITLE; + + // default canvas size is DEFAULT_SIZE-by-DEFAULT_SIZE + private static final int DEFAULT_SIZE = 512; + private static int width = DEFAULT_SIZE; + private static int height = DEFAULT_SIZE; + + // default pen radius + private static final double DEFAULT_PEN_RADIUS = 0.002; + + // current pen radius + private static double penRadius = DEFAULT_PEN_RADIUS; + + // show we draw immediately or wait until next show? + private static boolean defer = false; + + // boundary of drawing canvas, 0% border + // private static final double BORDER = 0.05; + private static final double BORDER = 0.00; + 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; + + private static double xmin = DEFAULT_XMIN; + private static double xmax = DEFAULT_XMAX; + private static double ymin = DEFAULT_YMIN; + private static double ymax = DEFAULT_YMAX; + + // for synchronization + private static final Object MOUSE_LOCK = new Object(); + private static final Object KEY_LOCK = new Object(); + + // default font + private static final Font DEFAULT_FONT = new Font("SansSerif", Font.PLAIN, 16); + + // current font + private static Font font = DEFAULT_FONT; + + // double buffered graphics + private static BufferedImage offscreenImage, onscreenImage; + private static Graphics2D offscreen, onscreen; + + // singleton for callbacks: avoids generation of extra .class files + private static StdDraw std = new StdDraw(); + + // the frame for drawing to the screen + private static JFrame frame; + + // is the JFrame visible (upon calling draw())? + private static boolean isJFrameVisible = true; + + // mouse state + private static boolean isMousePressed = false; + private static double mouseX = 0; + private static double mouseY = 0; + + // queue of typed key characters + private static LinkedList keysTyped = new LinkedList(); + + + // set of key codes currently pressed down + private static TreeSet keysDown = new TreeSet(); + + // singleton pattern: client can't instantiate + private StdDraw() { } + + + // static initializer + static { + initCanvas(); + initGUI(); + } + + /** + * Makes the drawing window visible or invisible. + * + * @param isVisible if {@code true}, makes the drawing window visible, + * otherwise hides the drawing window. + */ + public static void setVisible(boolean isVisible) { + isJFrameVisible = isVisible; + frame.setVisible(isVisible); + } + + /** + * Sets the canvas (drawing area) to be 512-by-512 pixels. + * This also clears the current drawing using the default background + * color (white). + * Ordinarily, this method is called once, at the very beginning + * of a program. + */ + public static void setCanvasSize() { + setCanvasSize(DEFAULT_SIZE, DEFAULT_SIZE); + } + + /** + * Sets the canvas (drawing area) to be width-by-height pixels. + * This also clears the current drawing using the default background + * color (white). + * Ordinarily, 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 static void setCanvasSize(int canvasWidth, int canvasHeight) { + if (canvasWidth <= 0) throw new IllegalArgumentException("width must be positive"); + if (canvasHeight <= 0) throw new IllegalArgumentException("height must be positive"); + width = canvasWidth; + height = canvasHeight; + initCanvas(); + initGUI(); + } + + // initialize the drawing canvas + private static void initCanvas() { + + // BufferedImage stuff + offscreenImage = new BufferedImage(2*width, 2*height, BufferedImage.TYPE_INT_ARGB); + onscreenImage = new BufferedImage(2*width, 2*height, BufferedImage.TYPE_INT_ARGB); + offscreen = offscreenImage.createGraphics(); + onscreen = onscreenImage.createGraphics(); + offscreen.scale(2.0, 2.0); // since we made it 2x as big + + // initialize drawing window + offscreen.setBackground(DEFAULT_BACKGROUND_COLOR); + offscreen.clearRect(0, 0, width, height); + onscreen.setBackground(DEFAULT_BACKGROUND_COLOR); + onscreen.clearRect(0, 0, 2*width, 2*height); + + // set the pen color + offscreen.setColor(DEFAULT_PEN_COLOR); + + // add antialiasing + RenderingHints hints = new RenderingHints(null); + hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + offscreen.addRenderingHints(hints); + } + + // initialize the GUI + private static void initGUI() { + + // create the JFrame (if necessary) + if (frame == null) { + frame = new JFrame(); + frame.addKeyListener(std); // JLabel cannot get keyboard focus + frame.setFocusTraversalKeysEnabled(false); // allow VK_TAB with isKeyPressed() + frame.setResizable(false); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // closes all windows + // frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); // closes only current window + frame.setTitle(windowTitle); + frame.setJMenuBar(createMenuBar()); + } + + // create the ImageIcon + RetinaImageIcon icon = new RetinaImageIcon(onscreenImage); + JLabel draw = new JLabel(icon); + draw.addMouseListener(std); + draw.addMouseMotionListener(std); + + // finsh up the JFrame + frame.setContentPane(draw); + frame.pack(); + frame.requestFocusInWindow(); + frame.setVisible(false); + } + + // create the menu bar + private static JMenuBar createMenuBar() { + JMenuBar menuBar = new JMenuBar(); + JMenu menu = new JMenu("File"); + menuBar.add(menu); + JMenuItem menuItem1 = new JMenuItem(" Save... "); + menuItem1.addActionListener(std); + // Java 11: use getMenuShortcutKeyMaskEx() + // Java 8: use getMenuShortcutKeyMask() + menuItem1.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); + menu.add(menuItem1); + return menuBar; + } + + /** + * Closes the standard drawing window. + * This allows the client program to terminate instead of requiring + * the user to close the standard drawing window manually. + * Drawing after calling this method will restore the previous window state. + */ + public static void close() { + frame.dispose(); + } + + /*************************************************************************** + * Input validation helper methods. + ***************************************************************************/ + + // throw an IllegalArgumentException if x is NaN or infinite + private static void validate(double x, String name) { + if (Double.isNaN(x)) throw new IllegalArgumentException(name + " is NaN"); + if (Double.isInfinite(x)) throw new IllegalArgumentException(name + " is infinite"); + } + + // throw an IllegalArgumentException if s is null + private static void validateNonnegative(double x, String name) { + if (x < 0) throw new IllegalArgumentException(name + " negative"); + } + + // throw an IllegalArgumentException if s is null + private static void validateNotNull(Object x, String name) { + if (x == null) throw new IllegalArgumentException(name + " is null"); + } + + + /*************************************************************************** + * Set the title of standard drawing window. + ***************************************************************************/ + + /** + * Sets the title of the standard drawing window to the specified string. + * + * @param title the title + * @throws IllegalArgumentException if {@code title} is {@code null} + */ + public static void setTitle(String title) { + validateNotNull(title, "title"); + frame.setTitle(title); + windowTitle = title; + } + + /*************************************************************************** + * User and screen coordinate systems. + ***************************************************************************/ + + /** + * Sets the x-scale to the default range (between 0.0 and 1.0). + */ + public static void setXscale() { + setXscale(DEFAULT_XMIN, DEFAULT_XMAX); + } + + /** + * Sets the y-scale to the default range (between 0.0 and 1.0). + */ + public static void setYscale() { + setYscale(DEFAULT_YMIN, DEFAULT_YMAX); + } + + /** + * Sets both the x-scale and y-scale to the default range + * (between 0.0 and 1.0). + */ + public static void setScale() { + setXscale(); + setYscale(); + } + + /** + * Sets the x-scale to the specified range. + * + * @param min the minimum value of the x-scale + * @param max the maximum value of the x-scale + * @throws IllegalArgumentException if {@code (max == min)} + * @throws IllegalArgumentException if either {@code min} or {@code max} is either NaN or infinite + */ + public static void setXscale(double min, double max) { + validate(min, "min"); + validate(max, "max"); + double size = max - min; + if (size == 0.0) throw new IllegalArgumentException("the min and max are the same"); + synchronized (MOUSE_LOCK) { + xmin = min - BORDER * size; + xmax = max + BORDER * size; + } + } + + /** + * Sets the y-scale to the specified range. + * + * @param min the minimum value of the y-scale + * @param max the maximum value of the y-scale + * @throws IllegalArgumentException if {@code (max == min)} + * @throws IllegalArgumentException if either {@code min} or {@code max} is either NaN or infinite + */ + public static void setYscale(double min, double max) { + validate(min, "min"); + validate(max, "max"); + double size = max - min; + if (size == 0.0) throw new IllegalArgumentException("the min and max are the same"); + synchronized (MOUSE_LOCK) { + ymin = min - BORDER * size; + ymax = max + BORDER * size; + } + } + + /** + * Sets both the x-scale and y-scale to the (same) specified range. + * + * @param min the minimum value of the x- and y-scales + * @param max the maximum value of the x- and y-scales + * @throws IllegalArgumentException if {@code (max == min)} + * @throws IllegalArgumentException if either {@code min} or {@code max} is either NaN or infinite + */ + public static void setScale(double min, double max) { + validate(min, "min"); + validate(max, "max"); + double size = max - min; + if (size == 0.0) throw new IllegalArgumentException("the min and max are the same"); + synchronized (MOUSE_LOCK) { + xmin = min - BORDER * size; + xmax = max + BORDER * size; + ymin = min - BORDER * size; + ymax = max + BORDER * size; + } + } + + // helper functions that scale from user coordinates to screen coordinates and back + private static double scaleX(double x) { return width * (x - xmin) / (xmax - xmin); } + private static double scaleY(double y) { return height * (ymax - y) / (ymax - ymin); } + private static double factorX(double w) { return w * width / Math.abs(xmax - xmin); } + private static double factorY(double h) { return h * height / Math.abs(ymax - ymin); } + private static double userX(double x) { return xmin + x * (xmax - xmin) / width; } + private static double userY(double y) { return ymax - y * (ymax - ymin) / height; } + + + /** + * Clears the screen using the default background color (white). + */ + public static void clear() { + clear(DEFAULT_BACKGROUND_COLOR); + } + + /** + * Clears the screen using the specified background color. + * To make the background transparent, use {@code StdDraw.TRANSPARENT}. + * + * @param color the color to make the background + * @throws IllegalArgumentException if {@code color} is {@code null} + */ + public static void clear(Color color) { + validateNotNull(color, "color"); + + backgroundColor = color; + + offscreen.setBackground(backgroundColor); + offscreen.clearRect(0, 0, width, height); + onscreen.setBackground(backgroundColor); + onscreen.clearRect(0, 0, 2*width, 2*height); + + draw(); + } + + /** + * Returns the current pen radius. + * + * @return the current value of the pen radius + */ + public static double getPenRadius() { + return penRadius; + } + + /** + * Sets the pen size to the default size (0.002). + * The pen is circular, so that lines have rounded ends, and when you set the + * pen radius and draw a point, you get a circle of the specified radius. + * The pen radius is not affected by coordinate scaling. + */ + public static void setPenRadius() { + setPenRadius(DEFAULT_PEN_RADIUS); + } + + /** + * Sets the radius of the pen to the specified size. + * The pen is circular, so that lines have rounded ends, and when you set the + * pen radius and draw a point, you get a circle of the specified radius. + * The pen radius is not affected by coordinate scaling. + * + * @param radius the radius of the pen + * @throws IllegalArgumentException if {@code radius} is negative, NaN, or infinite + */ + public static void setPenRadius(double radius) { + validate(radius, "pen radius"); + validateNonnegative(radius, "pen radius"); + + penRadius = radius; + float scaledPenRadius = (float) (radius * DEFAULT_SIZE); + BasicStroke stroke = new BasicStroke(scaledPenRadius, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); + // BasicStroke stroke = new BasicStroke(scaledPenRadius); + offscreen.setStroke(stroke); + } + + /** + * Returns the current pen color. + * + * @return the current pen color + */ + public static Color getPenColor() { + return penColor; + } + + /** + * Returns the current background color. + * + * @return the current background color + */ + public static Color getBackgroundColor() { + return backgroundColor; + } + + /** + * Sets the pen color to the default color (black). + */ + public static void setPenColor() { + setPenColor(DEFAULT_PEN_COLOR); + } + + /** + * Sets the pen color to the specified color. + *

+ * There are a number predefined pen colors, such as + * {@code StdDraw.BLACK}, {@code StdDraw.WHITE}, {@code StdDraw.RED}, + * {@code StdDraw.GREEN}, and {@code StdDraw.BLUE}. + * + * @param color the color to make the pen + * @throws IllegalArgumentException if {@code color} is {@code null} + */ + public static void setPenColor(Color color) { + validateNotNull(color, "color"); + penColor = color; + offscreen.setColor(penColor); + } + + /** + * Sets the pen color to the specified 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 {@code red}, {@code green}, + * or {@code blue} is outside its prescribed range + */ + public static void setPenColor(int red, int green, int blue) { + if (red < 0 || red >= 256) throw new IllegalArgumentException("red must be between 0 and 255"); + if (green < 0 || green >= 256) throw new IllegalArgumentException("green must be between 0 and 255"); + if (blue < 0 || blue >= 256) throw new IllegalArgumentException("blue must be between 0 and 255"); + setPenColor(new Color(red, green, blue)); + } + + /** + * Returns the current font. + * + * @return the current font + */ + public static Font getFont() { + return font; + } + + /** + * Sets the font to the default font (sans serif, 16 point). + */ + public static void setFont() { + setFont(DEFAULT_FONT); + } + + /** + * Sets the font to the specified value. + * + * @param font the font + * @throws IllegalArgumentException if {@code font} is {@code null} + */ + public static void setFont(Font font) { + validateNotNull(font, "font"); + StdDraw.font = font; + } + + + /*************************************************************************** + * Drawing geometric shapes. + ***************************************************************************/ + + /** + * Draws a line segment between (x0, y0) and + * (x1, y1). + * + * @param x0 the x-coordinate of one endpoint + * @param y0 the y-coordinate of one endpoint + * @param x1 the x-coordinate of the other endpoint + * @param y1 the y-coordinate of the other endpoint + * @throws IllegalArgumentException if any coordinate is either NaN or infinite + */ + public static void line(double x0, double y0, double x1, double y1) { + validate(x0, "x0"); + validate(y0, "y0"); + validate(x1, "x1"); + validate(y1, "y1"); + offscreen.draw(new Line2D.Double(scaleX(x0), scaleY(y0), scaleX(x1), scaleY(y1))); + draw(); + } + + /** + * Draws one pixel at (x, y). + * This method is private because pixels depend on the display. + * To achieve the same effect, set the pen radius to 0 and call {@code point()}. + * + * @param x the x-coordinate of the pixel + * @param y the y-coordinate of the pixel + * @throws IllegalArgumentException if {@code x} or {@code y} is either NaN or infinite + */ + private static void pixel(double x, double y) { + validate(x, "x"); + validate(y, "y"); + offscreen.fillRect((int) Math.round(scaleX(x)), (int) Math.round(scaleY(y)), 1, 1); + } + + /** + * Draws a point centered at (x, y). + * The point is a filled circle whose radius is equal to the pen radius. + * To draw a single-pixel point, first set the pen radius to 0. + * + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @throws IllegalArgumentException if either {@code x} or {@code y} is either NaN or infinite + */ + public static void point(double x, double y) { + validate(x, "x"); + validate(y, "y"); + + double xs = scaleX(x); + double ys = scaleY(y); + double r = penRadius; + float scaledPenRadius = (float) (r * DEFAULT_SIZE); + + // double ws = factorX(2*r); + // double hs = factorY(2*r); + // if (ws <= 1 && hs <= 1) pixel(x, y); + if (scaledPenRadius <= 1) pixel(x, y); + else offscreen.fill(new Ellipse2D.Double(xs - scaledPenRadius/2, ys - scaledPenRadius/2, + scaledPenRadius, scaledPenRadius)); + draw(); + } + + /** + * Draws a circle of the specified radius, centered at (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 radius the radius of the circle + * @throws IllegalArgumentException if {@code radius} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public static void circle(double x, double y, double radius) { + validate(x, "x"); + validate(y, "y"); + validate(radius, "radius"); + validateNonnegative(radius, "radius"); + + double xs = scaleX(x); + double ys = scaleY(y); + double ws = factorX(2*radius); + double hs = factorY(2*radius); + 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 the specified radius, centered at (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 radius the radius of the circle + * @throws IllegalArgumentException if {@code radius} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public static void filledCircle(double x, double y, double radius) { + validate(x, "x"); + validate(y, "y"); + validate(radius, "radius"); + validateNonnegative(radius, "radius"); + + double xs = scaleX(x); + double ys = scaleY(y); + double ws = factorX(2*radius); + double hs = factorY(2*radius); + 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 the specified semimajor and semiminor axes, + * centered at (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 {@code semiMajorAxis} + * or {@code semiMinorAxis} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public static void ellipse(double x, double y, double semiMajorAxis, double semiMinorAxis) { + validate(x, "x"); + validate(y, "y"); + validate(semiMajorAxis, "semimajor axis"); + validate(semiMinorAxis, "semiminor axis"); + validateNonnegative(semiMajorAxis, "semimajor axis"); + validateNonnegative(semiMinorAxis, "semiminor axis"); + + 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 a filled ellipse with the specified semimajor and semiminor axes, + * centered at (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 {@code semiMajorAxis} + * or {@code semiMinorAxis} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public static void filledEllipse(double x, double y, double semiMajorAxis, double semiMinorAxis) { + validate(x, "x"); + validate(y, "y"); + validate(semiMajorAxis, "semimajor axis"); + validate(semiMinorAxis, "semiminor axis"); + validateNonnegative(semiMajorAxis, "semimajor axis"); + validateNonnegative(semiMinorAxis, "semiminor axis"); + + 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 a circular arc of the specified radius, + * centered at (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 radius 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 {@code radius} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public static void arc(double x, double y, double radius, double angle1, double angle2) { + validate(x, "x"); + validate(y, "y"); + validate(radius, "arc radius"); + validate(angle1, "angle1"); + validate(angle2, "angle2"); + validateNonnegative(radius, "arc radius"); + + while (angle2 < angle1) angle2 += 360; + double xs = scaleX(x); + double ys = scaleY(y); + double ws = factorX(2*radius); + double hs = factorY(2*radius); + 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 the specified size, centered at (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 halfLength one half the length of any side of the square + * @throws IllegalArgumentException if {@code halfLength} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public static void square(double x, double y, double halfLength) { + validate(x, "x"); + validate(y, "y"); + validate(halfLength, "halfLength"); + validateNonnegative(halfLength, "half length"); + + double xs = scaleX(x); + double ys = scaleY(y); + double ws = factorX(2*halfLength); + double hs = factorY(2*halfLength); + 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 the specified size, centered at (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 halfLength one half the length of any side of the square + * @throws IllegalArgumentException if {@code halfLength} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public static void filledSquare(double x, double y, double halfLength) { + validate(x, "x"); + validate(y, "y"); + validate(halfLength, "halfLength"); + validateNonnegative(halfLength, "half length"); + + double xs = scaleX(x); + double ys = scaleY(y); + double ws = factorX(2*halfLength); + double hs = factorY(2*halfLength); + 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 the specified size, centered at (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 one half the width of the rectangle + * @param halfHeight one half the height of the rectangle + * @throws IllegalArgumentException if either {@code halfWidth} or {@code halfHeight} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public static void rectangle(double x, double y, double halfWidth, double halfHeight) { + validate(x, "x"); + validate(y, "y"); + validate(halfWidth, "halfWidth"); + validate(halfHeight, "halfHeight"); + validateNonnegative(halfWidth, "half width"); + validateNonnegative(halfHeight, "half height"); + + 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 the specified size, centered at (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 one half the width of the rectangle + * @param halfHeight one half the height of the rectangle + * @throws IllegalArgumentException if either {@code halfWidth} or {@code halfHeight} is negative + * @throws IllegalArgumentException if any argument is either NaN or infinite + */ + public static void filledRectangle(double x, double y, double halfWidth, double halfHeight) { + validate(x, "x"); + validate(y, "y"); + validate(halfWidth, "halfWidth"); + validate(halfHeight, "halfHeight"); + validateNonnegative(halfWidth, "half width"); + validateNonnegative(halfHeight, "half height"); + + 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 vertices + * (x0, y0), + * (x1, y1), ..., + * (xn–1, yn–1). + * + * @param x an array of all the x-coordinates of the polygon + * @param y an array of all the y-coordinates of the polygon + * @throws IllegalArgumentException unless {@code x[]} and {@code y[]} + * are of the same length + * @throws IllegalArgumentException if any coordinate is either NaN or infinite + * @throws IllegalArgumentException if either {@code x[]} or {@code y[]} is {@code null} + */ + public static void polygon(double[] x, double[] y) { + validateNotNull(x, "x-coordinate array"); + validateNotNull(y, "y-coordinate array"); + for (int i = 0; i < x.length; i++) validate(x[i], "x[" + i + "]"); + for (int i = 0; i < y.length; i++) validate(y[i], "y[" + i + "]"); + + int n1 = x.length; + int n2 = y.length; + if (n1 != n2) throw new IllegalArgumentException("arrays must be of the same length"); + int n = n1; + if (n == 0) return; + + 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 vertices + * (x0, y0), + * (x1, y1), ..., + * (xn–1, yn–1). + * + * @param x an array of all the x-coordinates of the polygon + * @param y an array of all the y-coordinates of the polygon + * @throws IllegalArgumentException unless {@code x[]} and {@code y[]} + * are of the same length + * @throws IllegalArgumentException if any coordinate is either NaN or infinite + * @throws IllegalArgumentException if either {@code x[]} or {@code y[]} is {@code null} + */ + public static void filledPolygon(double[] x, double[] y) { + validateNotNull(x, "x-coordinate array"); + validateNotNull(y, "y-coordinate array"); + for (int i = 0; i < x.length; i++) validate(x[i], "x[" + i + "]"); + for (int i = 0; i < y.length; i++) validate(y[i], "y[" + i + "]"); + + int n1 = x.length; + int n2 = y.length; + if (n1 != n2) throw new IllegalArgumentException("arrays must be of the same length"); + int n = n1; + if (n == 0) return; + + 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. + ***************************************************************************/ + // get an image from the given filename + private static Image getImage(String filename) { + if (filename == null) throw new IllegalArgumentException(); + + // to read from file + ImageIcon icon = new ImageIcon(filename); + + // try to read from URL + if (icon.getImageLoadStatus() != MediaTracker.COMPLETE) { + try { + URI uri = new URI(filename); + if (uri.isAbsolute()) { + URL url = uri.toURL(); + icon = new ImageIcon(url); + } + } + catch (MalformedURLException | URISyntaxException e) { + /* not a url */ + } + } + + // in case file is inside a .jar (classpath relative to StdDraw) + if (icon.getImageLoadStatus() != MediaTracker.COMPLETE) { + URL url = StdDraw.class.getResource(filename); + if (url != null) + icon = new ImageIcon(url); + } + + // in case file is inside a .jar (classpath relative to root of jar) + if (icon.getImageLoadStatus() != MediaTracker.COMPLETE) { + URL url = StdDraw.class.getResource("/" + filename); + if (url == null) throw new IllegalArgumentException("could not read image: '" + filename + "'"); + icon = new ImageIcon(url); + } + + return icon.getImage(); + } + + /*************************************************************************** + * [Summer 2016] Should we update to use ImageIO instead of ImageIcon()? + * Seems to have some issues loading images on some systems + * and slows things down on other systems. + * especially if you don't call ImageIO.setUseCache(false) + * One advantage is that it returns a BufferedImage. + ***************************************************************************/ +/* + private static BufferedImage getImage(String filename) { + if (filename == null) throw new IllegalArgumentException(); + + // from a file or URL + try { + URL url = new URL(filename); + BufferedImage image = ImageIO.read(url); + return image; + } + catch (IOException e) { + // ignore + } + + // in case file is inside a .jar (classpath relative to StdDraw) + try { + URL url = StdDraw.class.getResource(filename); + BufferedImage image = ImageIO.read(url); + return image; + } + catch (IOException e) { + // ignore + } + + // in case file is inside a .jar (classpath relative to root of jar) + try { + URL url = StdDraw.class.getResource("/" + filename); + BufferedImage image = ImageIO.read(url); + return image; + } + catch (IOException e) { + // ignore + } + throw new IllegalArgumentException("image " + filename + " not found"); + } +*/ + /** + * Draws the specified image centered at (x, y). + * The supported image formats are typically JPEG, PNG, GIF, TIFF, and BMP. + * As an optimization, the picture is cached, so there is no performance + * penalty for redrawing the same image multiple times (e.g., in an animation). + * However, if you change the picture file after drawing it, subsequent + * calls will draw the original picture. + * + * @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 filename is invalid + * @throws IllegalArgumentException if either {@code x} or {@code y} is either NaN or infinite + */ + public static void picture(double x, double y, String filename) { + validate(x, "x"); + validate(y, "y"); + validateNotNull(filename, "filename"); + + // BufferedImage image = getImage(filename); + Image image = getImage(filename); + double xs = scaleX(x); + double ys = scaleY(y); + // int ws = image.getWidth(); // can call only if image is a BufferedImage + // int hs = image.getHeight(); + int ws = image.getWidth(null); + int hs = image.getHeight(null); + 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 the specified image centered at (x, y), + * rotated given number of degrees. + * The supported image formats are typically JPEG, PNG, GIF, TIFF, and BMP. + * + * @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 filename is invalid + * @throws IllegalArgumentException if {@code x}, {@code y}, {@code degrees} is NaN or infinite + * @throws IllegalArgumentException if {@code filename} is {@code null} + */ + public static void picture(double x, double y, String filename, double degrees) { + validate(x, "x"); + validate(y, "y"); + validate(degrees, "degrees"); + validateNotNull(filename, "filename"); + + // BufferedImage image = getImage(filename); + Image image = getImage(filename); + double xs = scaleX(x); + double ys = scaleY(y); + // int ws = image.getWidth(); // can call only if image is a BufferedImage + // int hs = image.getHeight(); + int ws = image.getWidth(null); + int hs = image.getHeight(null); + 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 the specified image centered at (x, y), + * rescaled to the specified bounding box. + * The supported image formats are typically JPEG, PNG, GIF, TIFF, and BMP. + * + * @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 scaledWidth the width of the scaled image (in screen coordinates) + * @param scaledHeight the height of the scaled image (in screen coordinates) + * @throws IllegalArgumentException if either {@code scaledWidth} + * or {@code scaledHeight} is negative + * @throws IllegalArgumentException if the image filename is invalid + * @throws IllegalArgumentException if {@code x} or {@code y} is either NaN or infinite + * @throws IllegalArgumentException if {@code filename} is {@code null} + */ + public static void picture(double x, double y, String filename, double scaledWidth, double scaledHeight) { + validate(x, "x"); + validate(y, "y"); + validate(scaledWidth, "scaled width"); + validate(scaledHeight, "scaled height"); + validateNotNull(filename, "filename"); + validateNonnegative(scaledWidth, "scaled width"); + validateNonnegative(scaledHeight, "scaled height"); + + Image image = getImage(filename); + double xs = scaleX(x); + double ys = scaleY(y); + double ws = factorX(scaledWidth); + double hs = factorY(scaledHeight); + 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 the specified image centered at (x, y), rotated + * given number of degrees, and rescaled to the specified bounding box. + * The supported image formats are typically JPEG, PNG, GIF, TIFF, and BMP. + * + * @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 scaledWidth the width of the scaled image (in screen coordinates) + * @param scaledHeight the height of the scaled image (in screen coordinates) + * @param degrees is the number of degrees to rotate counterclockwise + * @throws IllegalArgumentException if either {@code scaledWidth} + * or {@code scaledHeight} is negative + * @throws IllegalArgumentException if the image filename is invalid + */ + public static void picture(double x, double y, String filename, double scaledWidth, double scaledHeight, double degrees) { + validate(x, "x"); + validate(y, "y"); + validate(scaledWidth, "scaled width"); + validate(scaledHeight, "scaled height"); + validate(degrees, "degrees"); + validateNotNull(filename, "filename"); + validateNonnegative(scaledWidth, "scaled width"); + validateNonnegative(scaledHeight, "scaled height"); + + Image image = getImage(filename); + double xs = scaleX(x); + double ys = scaleY(y); + double ws = factorX(scaledWidth); + double hs = factorY(scaledHeight); + 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 at (x, y). + * + * @param x the center x-coordinate of the text + * @param y the center y-coordinate of the text + * @param text the text to write + * @throws IllegalArgumentException if {@code text} is {@code null} + * @throws IllegalArgumentException if {@code x} or {@code y} is either NaN or infinite + */ + public static void text(double x, double y, String text) { + validate(x, "x"); + validate(y, "y"); + validateNotNull(text, "text"); + + offscreen.setFont(font); + FontMetrics metrics = offscreen.getFontMetrics(); + double xs = scaleX(x); + double ys = scaleY(y); + int ws = metrics.stringWidth(text); + int hs = metrics.getDescent(); + offscreen.drawString(text, (float) (xs - ws/2.0), (float) (ys + hs)); + draw(); + } + + /** + * Writes the given text string in the current font, centered at (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 text the text to write + * @param degrees is the number of degrees to rotate counterclockwise + * @throws IllegalArgumentException if {@code text} is {@code null} + * @throws IllegalArgumentException if {@code x}, {@code y}, or {@code degrees} is either NaN or infinite + */ + public static void text(double x, double y, String text, double degrees) { + validate(x, "x"); + validate(y, "y"); + validate(degrees, "degrees"); + validateNotNull(text, "text"); + + double xs = scaleX(x); + double ys = scaleY(y); + offscreen.rotate(Math.toRadians(-degrees), xs, ys); + text(x, y, text); + 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 text the text + * @throws IllegalArgumentException if {@code text} is {@code null} + * @throws IllegalArgumentException if {@code x} or {@code y} is either NaN or infinite + */ + public static void textLeft(double x, double y, String text) { + validate(x, "x"); + validate(y, "y"); + validateNotNull(text, "text"); + + offscreen.setFont(font); + FontMetrics metrics = offscreen.getFontMetrics(); + double xs = scaleX(x); + double ys = scaleY(y); + int hs = metrics.getDescent(); + offscreen.drawString(text, (float) xs, (float) (ys + hs)); + draw(); + } + + /** + * Writes the given text string in the current font, right-aligned at (x, y). + * + * @param x the x-coordinate of the text + * @param y the y-coordinate of the text + * @param text the text to write + * @throws IllegalArgumentException if {@code text} is {@code null} + * @throws IllegalArgumentException if {@code x} or {@code y} is either NaN or infinite + */ + public static void textRight(double x, double y, String text) { + validate(x, "x"); + validate(y, "y"); + validateNotNull(text, "text"); + + offscreen.setFont(font); + FontMetrics metrics = offscreen.getFontMetrics(); + double xs = scaleX(x); + double ys = scaleY(y); + int ws = metrics.stringWidth(text); + int hs = metrics.getDescent(); + offscreen.drawString(text, (float) (xs - ws), (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 + * @throws IllegalArgumentException if {@code t} is negative + * @deprecated replaced by {@link #enableDoubleBuffering()}, {@link #show()}, and {@link #pause(int t)} + */ + @Deprecated + public static void show(int t) { + validateNonnegative(t, "t"); + show(); + pause(t); + enableDoubleBuffering(); + } + + /** + * Pauses for t milliseconds. This method is intended to support computer animations. + * @param t number of milliseconds + * @throws IllegalArgumentException if {@code t} is negative + */ + public static void pause(int t) { + validateNonnegative(t, "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 static void show() { + onscreen.drawImage(offscreenImage, 0, 0, null); + + // make frame visible upon first call to show() + if (frame.isVisible() != isJFrameVisible) { + frame.setVisible(isJFrameVisible); + } + + frame.repaint(); + } + + // draw onscreen if defer is false + private static void draw() { + if (!defer) show(); + } + + /** + * Enables double buffering. All subsequent calls to + * drawing methods such as {@code line()}, {@code circle()}, + * and {@code square()} will be deferred until the next call + * to show(). Useful for animations. + */ + public static void enableDoubleBuffering() { + defer = true; + } + + /** + * Disables 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 static void disableDoubleBuffering() { + defer = false; + } + + + /*************************************************************************** + * Save drawing to a file. + ***************************************************************************/ + + /** + * Saves the drawing to a file in a supported file format + * (typically JPEG, PNG, GIF, TIFF, and BMP). + * The filetype extension must be {@code .jpg}, {@code .png}, {@code .gif}, + * {@code .bmp}, or {@code .tif}. + * + * @param filename the name of the file + * @throws IllegalArgumentException if {@code filename} is {@code null} + * @throws IllegalArgumentException if {@code filename} is the empty string + * @throws IllegalArgumentException if {@code filename} has invalid filetype extension + * @throws IllegalArgumentException if cannot write the file {@code filename} + */ + public static void save(String filename) { + validateNotNull(filename, "filename"); + if (filename.length() == 0) { + throw new IllegalArgumentException("argument to save() is the empty string"); + } + + File file = new File(filename); + String suffix = filename.substring(filename.lastIndexOf('.') + 1); + if (!filename.contains(".") || suffix.length() == 0) { + throw new IllegalArgumentException("The filename '" + filename + "' has no filetype extension, such as .jpg or .png"); + } + + try { + // if the file format supports transparency (such as PNG or GIF) + if (ImageIO.write(onscreenImage, suffix, file)) return; + + // if the file format does not support transparency (such as JPEG or BMP) + BufferedImage saveImage = new BufferedImage(2*width, 2*height, BufferedImage.TYPE_INT_RGB); + saveImage.createGraphics().drawImage(onscreenImage, 0, 0, Color.WHITE, null); + if (ImageIO.write(saveImage, suffix, file)) return; + + // failed to save the file; probably wrong format + throw new IllegalArgumentException("The filetype '" + suffix + "' is not supported"); + } + catch (IOException e) { + throw new IllegalArgumentException("could not write file '" + filename + "'", e); + } + } + + + /** + * This method cannot be called directly. + */ + @Override + public void actionPerformed(ActionEvent event) { + FileDialog chooser = new FileDialog(StdDraw.frame, "Use a .png or .jpg extension", FileDialog.SAVE); + chooser.setVisible(true); + String selectedDirectory = chooser.getDirectory(); + String selectedFilename = chooser.getFile(); + if (selectedDirectory != null && selectedFilename != null) { + try { + StdDraw.save(selectedDirectory + selectedFilename); + } + catch (IllegalArgumentException e) { + System.err.println(e.getMessage()); + } + } + } + + + /*************************************************************************** + * Mouse interactions. + ***************************************************************************/ + + /** + * Returns true if the mouse is being pressed. + * + * @return {@code true} if the mouse is being pressed; {@code false} otherwise + */ + public static boolean isMousePressed() { + synchronized (MOUSE_LOCK) { + 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 static boolean mousePressed() { + synchronized (MOUSE_LOCK) { + return isMousePressed; + } + } + + /** + * Returns the x-coordinate of the mouse. + * + * @return the x-coordinate of the mouse + */ + public static double mouseX() { + synchronized (MOUSE_LOCK) { + return mouseX; + } + } + + /** + * Returns the y-coordinate of the mouse. + * + * @return y-coordinate of the mouse + */ + public static double mouseY() { + synchronized (MOUSE_LOCK) { + return mouseY; + } + } + + + /** + * This method cannot be called directly. + */ + @Override + public void mouseClicked(MouseEvent event) { + // this body is intentionally left empty + } + + /** + * This method cannot be called directly. + */ + @Override + public void mouseEntered(MouseEvent event) { + // this body is intentionally left empty + } + + /** + * This method cannot be called directly. + */ + @Override + public void mouseExited(MouseEvent event) { + // this body is intentionally left empty + } + + /** + * This method cannot be called directly. + */ + @Override + public void mousePressed(MouseEvent event) { + synchronized (MOUSE_LOCK) { + mouseX = StdDraw.userX(event.getX()); + mouseY = StdDraw.userY(event.getY()); + isMousePressed = true; + } + } + + /** + * This method cannot be called directly. + */ + @Override + public void mouseReleased(MouseEvent event) { + synchronized (MOUSE_LOCK) { + isMousePressed = false; + } + } + + /** + * This method cannot be called directly. + */ + @Override + public void mouseDragged(MouseEvent event) { + synchronized (MOUSE_LOCK) { + mouseX = StdDraw.userX(event.getX()); + mouseY = StdDraw.userY(event.getY()); + } + } + + /** + * This method cannot be called directly. + */ + @Override + public void mouseMoved(MouseEvent event) { + synchronized (MOUSE_LOCK) { + mouseX = StdDraw.userX(event.getX()); + mouseY = StdDraw.userY(event.getY()); + } + } + + + /*************************************************************************** + * Keyboard interactions. + ***************************************************************************/ + + /** + * Returns true if the user has typed a key (that has not yet been processed). + * + * @return {@code true} if the user has typed a key (that has not yet been processed + * by {@link #nextKeyTyped()}; {@code false} otherwise + */ + public static boolean hasNextKeyTyped() { + synchronized (KEY_LOCK) { + return !keysTyped.isEmpty(); + } + } + + /** + * Returns the next key that was typed by the user (that your program has not already processed). + * This method should be preceded by a call to {@link #hasNextKeyTyped()} to ensure + * that there is a next key to process. + * This method returns a Unicode character corresponding to the key + * typed (such as {@code 'a'} or {@code 'A'}). + * It cannot identify action keys (such as F1 and arrow keys) + * or modifier keys (such as control). + * + * @return the next key typed by the user (that your program has not already processed). + * @throws NoSuchElementException if there is no remaining key + */ + public static char nextKeyTyped() { + synchronized (KEY_LOCK) { + if (keysTyped.isEmpty()) { + throw new NoSuchElementException("your program has already processed all keystrokes"); + } + return keysTyped.remove(keysTyped.size() - 1); + // return keysTyped.removeLast(); + } + } + + /** + * Returns true if the given key is being pressed. + *

+ * This method takes the keycode (corresponding to a physical key) + * as an argument. 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 key to check if it is being pressed + * @return {@code true} if {@code keycode} is currently being pressed; + * {@code false} otherwise + */ + public static boolean isKeyPressed(int keycode) { + synchronized (KEY_LOCK) { + return keysDown.contains(keycode); + } + } + + + /** + * This method cannot be called directly. + */ + @Override + public void keyTyped(KeyEvent event) { + synchronized (KEY_LOCK) { + keysTyped.addFirst(event.getKeyChar()); + } + } + + /** + * This method cannot be called directly. + */ + @Override + public void keyPressed(KeyEvent event) { + synchronized (KEY_LOCK) { + keysDown.add(event.getKeyCode()); + } + } + + /** + * This method cannot be called directly. + */ + @Override + public void keyReleased(KeyEvent event) { + synchronized (KEY_LOCK) { + keysDown.remove(event.getKeyCode()); + } + } + + + /*************************************************************************** + * For improved resolution on Mac Retina displays. + ***************************************************************************/ + + private static class RetinaImageIcon extends ImageIcon { + + public RetinaImageIcon(Image image) { + super(image); + } + + public int getIconWidth() { + return super.getIconWidth() / 2; + } + + /** + * Returns the height of the icon. + * + * @return the height in pixels of this icon + */ + public int getIconHeight() { + return super.getIconHeight() / 2; + } + + public synchronized void paintIcon(Component c, Graphics g, int x, int y) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g2.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON); + g2.scale(0.5, 0.5); + super.paintIcon(c, g2, x * 2, y * 2); + g2.dispose(); + } + } + + + /** + * Test client. + * + * @param args the command-line arguments + */ + public static void main(String[] args) { + StdDraw.square(0.2, 0.8, 0.1); + StdDraw.filledSquare(0.8, 0.8, 0.2); + StdDraw.circle(0.8, 0.2, 0.2); + + StdDraw.setPenColor(StdDraw.BOOK_RED); + StdDraw.setPenRadius(0.02); + StdDraw.arc(0.8, 0.2, 0.1, 200, 45); + + // draw a blue diamond + StdDraw.setPenRadius(); + StdDraw.setPenColor(StdDraw.BOOK_BLUE); + double[] x = { 0.1, 0.2, 0.3, 0.2 }; + double[] y = { 0.2, 0.3, 0.2, 0.1 }; + StdDraw.filledPolygon(x, y); + + // text + StdDraw.setPenColor(StdDraw.BLACK); + StdDraw.text(0.2, 0.5, "black text"); + StdDraw.setPenColor(StdDraw.WHITE); + StdDraw.text(0.8, 0.8, "white text"); + } + +} diff --git a/common/common.typ b/common/common.typ index 85f02a7..eae5ece 100644 --- a/common/common.typ +++ b/common/common.typ @@ -6,8 +6,12 @@ #doc ] -#let embedClass(name: str) = { +#let embedClass(name: str, label: none) = { + show figure: set block(width: 100%) show figure: set align(left) show figure.caption: set align(center) - figure(caption: name, raw(read("../" + name + ".java"), lang:"java", block: true)) + [ + #figure(caption: name, kind: "Class", supplement: [Class], raw(read("../" + name + ".java"), lang:"java", block: true)) + #label + ] } \ No newline at end of file diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..7631554 --- /dev/null +++ b/shell.nix @@ -0,0 +1,11 @@ +{ + pkgs ? import { }, +}: +pkgs.mkShell rec { + buildInputs = with pkgs; [ + jdk + ]; + nativeBuildInputs = with pkgs; [ + jre + ]; +} diff --git a/week4/Card.java b/week4/Card.java index b428bb4..aeef091 100644 --- a/week4/Card.java +++ b/week4/Card.java @@ -67,6 +67,7 @@ public class Card { var top = ""; top += value; if(this.value > 10) { + // face cards are empty top += " "; } else { // and for numbered cards, show the suit characters @@ -92,7 +93,7 @@ public class Card { // instead of using 3 rows we have a gap in the 3rd row output += " "; output += this.value >= 6 ? suit : " "; - // odd or 10 + // odd or 10 have a symbol in the middle output += this.value % 2 == 1 || this.value == 10 ? suit : " "; output += this.value >= 6 ? suit : " "; output += " "; @@ -113,7 +114,8 @@ public class Card { } // Shows cards next to each other (left to right) - // assumes that sprintCard returns the same width for each card (and each row of a card) + // for visuals, assumes that sprintCard returns the same width for each card (and each row of a card) + // for correctness, assumes that sprintCard always returns 4 rows (is asserted) public static String sprintCards(Card[] cards) { String[] output = { "", "", "", "" }; @@ -121,6 +123,7 @@ public class Card { // save the row into relevant output for(var i = 0; i < cards.length; i++) { var cardstr = cards[i].sprintCard().split("\n"); + assert output.length == cardstr.length; for(var x = 0; x < cardstr.length; x++) { output[x] += cardstr[x] + " "; } diff --git a/week4/Transpose.java b/week4/Transpose.java index 57db44f..d5efd05 100644 --- a/week4/Transpose.java +++ b/week4/Transpose.java @@ -1,3 +1,24 @@ public class Transpose { - + public static void main(String[] args) { + int[][] input = { + {99, 85, 98}, + {98, 57, 78}, + {92, 77, 76}, + }; + + for(var i = 0; i < input.length; i++) { + for(var j = i; j < input.length; j++) { + var og = input[i][j]; + input[i][j] = input[j][i]; + input[j][i] = og; + } + } + + for(var i = 0; i < input.length; i++) { + for(var j = 0; j < input.length; j++) { + System.out.print(input[i][j] + " "); + } + System.out.println(); + } + } } diff --git a/week4/doc.typ b/week4/doc.typ index 7fe39d3..f92bd6b 100644 --- a/week4/doc.typ +++ b/week4/doc.typ @@ -9,32 +9,50 @@ Write a program `Deal` that takes an integer command-line argument `n` and prints `n` poker hands (five cards each) from a shuffled deck, separated by blank lines. +A second class Card (@card) is used for visuals. + +Example run for 2 hands (`java Deal.java 2`): + +``` +J J K K ⒑♥ ♥⒑ 9♦ ♦9 7♠ ♠7 +♥ ♥ ♠ ♠ ♥♥♥ ♦♦♦ ♠♠♠ +♥ ♥ ♠ ♠ ♥♥♥ ♦ ♦ +J J K K ⒑♥ ♥⒑ 9♦ ♦9 7♠ ♠7 + +3 ♥ 3 4♠ ♠4 4♦ ♦4 7♦ ♦7 6♦ ♦6 + ♥ ♦♦♦ ♦ ♦ + +3 ♥ 3 4♠ ♠4 4♦ ♦4 7♦ ♦7 6♦ ♦6 +``` + #embedClass(name: "Deal") -#embedClass(name: "Card") +#embedClass(name: "Card", label: ) == Exercise 1.4.14 Write a code fragment to print the transposition (rows and columns exchanged) of a square two-dimensional array. -For the example spreadsheet array in the text, you code would print the following: -``` -99 98 92 94 99 90 76 92 97 89 -85 57 77 32 34 46 59 66 71 29 -98 78 76 11 22 54 88 89 24 38 -``` +#place( + right, + figure( + table( + columns: 2, + [Example input], + [Output], + ``` + 99 85 98 + 98 57 78 + 92 77 76 + ```, + ``` + 99 98 92 + 85 57 77 + 98 78 76 + ``` + ), + caption: [Example input and output matrix] + ) +) -Input: - -``` -99 85 98 -98 57 78 -92 77 76 -94 32 11 -99 34 22 -90 46 54 -76 59 88 -92 66 89 -97 71 24 -89 29 38 -``` +#embedClass(name: "Transpose") \ No newline at end of file diff --git a/week4/tests/Transpose/input1.txt b/week4/tests/Transpose/input1.txt index f010698..13ec9f8 100644 --- a/week4/tests/Transpose/input1.txt +++ b/week4/tests/Transpose/input1.txt @@ -1,10 +1,3 @@ 99 85 98 98 57 78 -92 77 76 -94 32 11 -99 34 22 -90 46 54 -76 59 88 -92 66 89 -97 71 24 -89 29 38 \ No newline at end of file +92 77 76 \ No newline at end of file diff --git a/week4/tests/Transpose/output1.txt b/week4/tests/Transpose/output1.txt index 744cb60..08ccf10 100644 --- a/week4/tests/Transpose/output1.txt +++ b/week4/tests/Transpose/output1.txt @@ -1,3 +1,3 @@ -99 98 92 94 99 90 76 92 97 89 -85 57 77 32 34 46 59 66 71 29 -98 78 76 11 22 54 88 89 24 38 \ No newline at end of file +99 98 92 +85 57 77 +98 78 76 \ No newline at end of file diff --git a/week5/CircleLines.java b/week5/CircleLines.java new file mode 100644 index 0000000..e667e07 --- /dev/null +++ b/week5/CircleLines.java @@ -0,0 +1,38 @@ +package week5; + +import common.StdDraw; + +public class CircleLines { + public static void main(String[] args) { + int n = Integer.parseInt(args[0]); + double p = Double.parseDouble(args[1]); + assert p >= 0. && p <= 1.; + StdDraw.setXscale(-1.1, 1.1); + StdDraw.setYscale(-1.1, 1.1); + StdDraw.clear(); + double[][] points = new double[n][2]; + + for(var i = 0; i < n; i++) { + // explicitly convert to double + double fi = i; + // divide by total to get 0-1 + fi /= n; + // and convert to radians + fi *= 2*Math.PI; + // save calculations + points[i][0] = Math.sin(fi); + points[i][1] = Math.cos(fi); + StdDraw.filledCircle(points[i][0], points[i][1], 0.01); + } + + StdDraw.setPenColor(StdDraw.GRAY); + + for(var i = 0; i < n; i++) { + for(var x = i; x < n; x++) { + if(Math.random() > p) continue; + + StdDraw.line(points[i][0], points[i][1], points[x][0], points[x][1]); + } + } + } +} diff --git a/week5/Circles.java b/week5/Circles.java new file mode 100644 index 0000000..809bb22 --- /dev/null +++ b/week5/Circles.java @@ -0,0 +1,27 @@ +package week5; + +import common.StdDraw; + +public class Circles { + public static void main(String[] args) { + var circleCount = Integer.parseInt(args[0]); + var pBlack = Double.parseDouble(args[1]); + var minRadius = Double.parseDouble(args[2]); + var maxRadius = Double.parseDouble(args[3]); + + StdDraw.setScale(); + StdDraw.clear(); + + for(var i = 0; i < circleCount; i++) { + if(Math.random() < pBlack) { + StdDraw.setPenColor(StdDraw.BLACK); + } else { + StdDraw.setPenColor(StdDraw.WHITE); + } + var radii = Math.random() * (maxRadius - minRadius) + minRadius; + var posX = Math.random(); + var posY = Math.random(); + StdDraw.filledCircle(posX, posY, radii); + } + } +} diff --git a/week5/MissingInt.java b/week5/MissingInt.java new file mode 100644 index 0000000..df7215a --- /dev/null +++ b/week5/MissingInt.java @@ -0,0 +1,19 @@ +package week5; + +import common.StdIn; + +public class MissingInt { + public static void main(String[] args) { + int n = Integer.parseInt(args[0]); + boolean[] nums = new boolean[n]; + for(var i = 0; i < n - 1; i++) { + int integer = StdIn.readInt() - 1; + nums[integer] = true; + } + for(var i = 0; i < nums.length; i++) { + if(!nums[i]) { + System.err.println(i + 1); + } + } + } +} diff --git a/week5/common b/week5/common new file mode 120000 index 0000000..60d3b0a --- /dev/null +++ b/week5/common @@ -0,0 +1 @@ +../common \ No newline at end of file diff --git a/week5/doc.typ b/week5/doc.typ new file mode 100644 index 0000000..43f5f2b --- /dev/null +++ b/week5/doc.typ @@ -0,0 +1,30 @@ +#import "./common/common.typ" : * + +#show: template + += Week 5 + +== Exercise 1.5.7 + +Write a program that takes an integer command-line argument $n$, reads in +$n-1$ distinct integers between 1 and $n$, and determines the missing value. + +#embedClass(name: "MissingInt") + +== Exercise 1.5.19 + +Write a program that takes as command-line arguments an integer $n$ and +a floating-point number $p$ (between $0$ and $1$), plots $n$ equally spaced points on the +circumference of a circle, and then, with probability $p$ for each pair of points, draws +a gray line connecting them. + +#embedClass(name: "CircleLines") + +== Exercise 1.5.26 + +Write a program Circles that draws filled circles of random radii at random +positions in the unit square, producing images like those below. Your program +should take four command-line arguments: the number of circles, the probability +that each circle is black, the minimum radius, and the maximum radius. + +#embedClass(name: "Circles") \ No newline at end of file