mirror of
https://github.com/danbulant/introductionToProgramming
synced 2026-05-19 04:18:32 +00:00
1881 lines
64 KiB
Java
1881 lines
64 KiB
Java
/******************************************************************************
|
|
* 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 <code>Draw</code> 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.
|
|
* <p>
|
|
* For additional documentation, see
|
|
* <a href="https://introcs.cs.princeton.edu/31datatype">Section 3.1</a> of
|
|
* <i>Computer Science: An Interdisciplinary Approach</i> 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 <em>Introduction to Programming in Java</em>.
|
|
* 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 <em>Introduction to Programming in Java</em>.
|
|
* 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 <em>Algorithms, 4th edition</em>.
|
|
* 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<Character> keysTyped = new LinkedList<Character>();
|
|
private final TreeSet<Integer> keysDown = new TreeSet<Integer>();
|
|
|
|
// event-based listeners
|
|
private final ArrayList<DrawListener> listeners = new ArrayList<DrawListener>();
|
|
|
|
// 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 <em>width</em>-by-<em>height</em> 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 (<em>x</em>, <em>y</em>).
|
|
*
|
|
* @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 (<em>x</em>, <em>y</em>).
|
|
*
|
|
* @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 (<em>x</em>, <em>y</em>).
|
|
*
|
|
* @param x the <em>x</em>-coordinate of the center of the ellipse
|
|
* @param y the <em>y</em>-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 (<em>x</em>, <em>y</em>).
|
|
*
|
|
* @param x the <em>x</em>-coordinate of the center of the ellipse
|
|
* @param y the <em>y</em>-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 (<em>x</em>, <em>y</em>), from angle1 to angle2 (in degrees).
|
|
*
|
|
* @param x the <em>x</em>-coordinate of the center of the circle
|
|
* @param y the <em>y</em>-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 (<em>x</em>, <em>y</em>).
|
|
*
|
|
* @param x the <em>x</em>-coordinate of the center of the square
|
|
* @param y the <em>y</em>-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 (<em>x</em>, <em>y</em>).
|
|
*
|
|
* @param x the <em>x</em>-coordinate of the center of the square
|
|
* @param y the <em>y</em>-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 (<em>x</em>, <em>y</em>).
|
|
*
|
|
* @param x the <em>x</em>-coordinate of the center of the rectangle
|
|
* @param y the <em>y</em>-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 (<em>x</em>, <em>y</em>).
|
|
*
|
|
* @param x the <em>x</em>-coordinate of the center of the rectangle
|
|
* @param y the <em>y</em>-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
|
|
* (<em>x</em><sub>0</sub>, <em>y</em><sub>0</sub>),
|
|
* (<em>x</em><sub>1</sub>, <em>y</em><sub>1</sub>), ...,
|
|
* (<em>x</em><sub><em>n</em>–1</sub>, <em>y</em><sub><em>n</em>–1</sub>).
|
|
*
|
|
* @param x an array of all the <em>x</em>-coordinates of the polygon
|
|
* @param y an array of all the <em>y</em>-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
|
|
* (<em>x</em><sub>0</sub>, <em>y</em><sub>0</sub>),
|
|
* (<em>x</em><sub>1</sub>, <em>y</em><sub>1</sub>), ...,
|
|
* (<em>x</em><sub><em>n</em>–1</sub>, <em>y</em><sub><em>n</em>–1</sub>).
|
|
*
|
|
* @param x an array of all the <em>x</em>-coordinates of the polygon
|
|
* @param y an array of all the <em>y</em>-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 (<em>x</em>, <em>y</em>).
|
|
* 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 <em>x</em>-coordinate of the image
|
|
* @param y the center <em>y</em>-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 (<em>x</em>, <em>y</em>),
|
|
* rotated given number of degrees.
|
|
* The supported image formats are typically JPEG, PNG, GIF, TIFF, and BMP.
|
|
*
|
|
* @param x the center <em>x</em>-coordinate of the image
|
|
* @param y the center <em>y</em>-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 (<em>x</em>, <em>y</em>),
|
|
* rescaled to the specified bounding box.
|
|
* The supported image formats are typically JPEG, PNG, GIF, TIFF, and BMP.
|
|
*
|
|
* @param x the center <em>x</em>-coordinate of the image
|
|
* @param y the center <em>y</em>-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 (<em>x</em>, <em>y</em>), 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 <em>x</em>-coordinate of the image
|
|
* @param y the center <em>y</em>-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 (<em>x</em>, <em>y</em>).
|
|
*
|
|
* @param x the center <em>x</em>-coordinate of the text
|
|
* @param y the center <em>y</em>-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 (<em>x</em>, <em>y</em>) and
|
|
* rotated by the specified number of degrees.
|
|
* @param x the center <em>x</em>-coordinate of the text
|
|
* @param y the center <em>y</em>-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 (<em>x</em>, <em>y</em>).
|
|
* @param x the <em>x</em>-coordinate of the text
|
|
* @param y the <em>y</em>-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 (<em>x</em>, <em>y</em>).
|
|
*
|
|
* @param x the <em>x</em>-coordinate of the text
|
|
* @param y the <em>y</em>-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.
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* @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");
|
|
}
|
|
|
|
}
|