001package stdlib;
002/* ***********************************************************************
003 *  Compilation:  javac StdDraw.java
004 *  Execution:    java StdDraw
005 *
006 *  Standard drawing library. This class provides a basic capability for
007 *  creating drawings with your programs. It uses a simple graphics model that
008 *  allows you to create drawings consisting of points, lines, and curves
009 *  in a window on your computer and to save the drawings to a file.
010 *
011 *  Todo
012 *  ----
013 *    -  Add support for gradient fill, etc.
014 *
015 *  Remarks
016 *  -------
017 *    -  don't use AffineTransform for rescaling since it inverts
018 *       images and strings
019 *    -  careful using setFont in inner loop within an animation -
020 *       it can cause flicker
021 *
022 *************************************************************************/
023
024import java.awt.*;
025import java.awt.event.*;
026import java.awt.geom.*;
027import java.awt.image.*;
028import java.io.*;
029import java.net.*;
030import java.util.LinkedList;
031import java.util.TreeSet;
032import javax.imageio.ImageIO;
033import javax.swing.*;
034
035/**
036 *  <i>Standard draw</i>. This class provides a basic capability for
037 *  creating drawings with your programs. It uses a simple graphics model that
038 *  allows you to create drawings consisting of points, lines, and curves
039 *  in a window on your computer and to save the drawings to a file.
040 *  <p>
041 *  For additional documentation, see <a href="http://introcs.cs.princeton.edu/15inout">Section 1.5</a> of
042 *  <i>Introduction to Programming in Java: An Interdisciplinary Approach</i> by Robert Sedgewick and Kevin Wayne.
043 */
044public final class StdDraw implements ActionListener, MouseListener, MouseMotionListener, KeyListener {
045
046        // pre-defined colors
047        public static final Color BLACK      = Color.BLACK;
048        public static final Color BLUE       = Color.BLUE;
049        public static final Color CYAN       = Color.CYAN;
050        public static final Color DARK_GRAY  = Color.DARK_GRAY;
051        public static final Color GRAY       = Color.GRAY;
052        public static final Color GREEN      = Color.GREEN;
053        public static final Color LIGHT_GRAY = Color.LIGHT_GRAY;
054        public static final Color MAGENTA    = Color.MAGENTA;
055        public static final Color ORANGE     = Color.ORANGE;
056        public static final Color PINK       = Color.PINK;
057        public static final Color RED        = Color.RED;
058        public static final Color WHITE      = Color.WHITE;
059        public static final Color YELLOW     = Color.YELLOW;
060
061        /**
062         * Shade of blue used in Introduction to Programming in Java.
063         * It is Pantone 300U. The RGB values are approximately (9, 90, 166).
064         */
065        public static final Color BOOK_BLUE       = new Color(  9,  90, 166);
066        public static final Color BOOK_LIGHT_BLUE = new Color(103, 198, 243);
067
068        /**
069         * Shade of red used in Algorithms 4th edition.
070         * It is Pantone 1805U. The RGB values are approximately (150, 35, 31).
071         */
072        public static final Color BOOK_RED = new Color(150, 35, 31);
073
074        // default colors
075        private static final Color DEFAULT_PEN_COLOR   = BLACK;
076        private static final Color DEFAULT_CLEAR_COLOR = WHITE;
077
078        // current pen color
079        private static Color penColor;
080
081        // default canvas size is DEFAULT_SIZE-by-DEFAULT_SIZE
082        private static final int DEFAULT_SIZE = 512;
083        private static int width  = DEFAULT_SIZE;
084        private static int height = DEFAULT_SIZE;
085
086        // default pen radius
087        private static final double DEFAULT_PEN_RADIUS = 0.002;
088
089        // current pen radius
090        private static double penRadius;
091
092        // show we draw immediately or wait until next show?
093        private static boolean defer = false;
094
095        // boundary of drawing canvas, 5% border
096        private static final double BORDER = 0.05;
097        private static final double DEFAULT_XMIN = 0.0;
098        private static final double DEFAULT_XMAX = 1.0;
099        private static final double DEFAULT_YMIN = 0.0;
100        private static final double DEFAULT_YMAX = 1.0;
101        private static double xmin, ymin, xmax, ymax;
102
103        // for synchronization
104        private static Object mouseLock = new Object();
105        private static Object keyLock = new Object();
106
107        // default font
108        private static final Font DEFAULT_FONT = new Font("SansSerif", Font.PLAIN, 16);
109
110        // current font
111        private static Font font;
112
113        // double buffered graphics
114        private static BufferedImage offscreenImage, onscreenImage;
115        private static Graphics2D offscreen, onscreen;
116
117        // singleton for callbacks: avoids generation of extra .class files
118        private static StdDraw std = new StdDraw();
119
120        // the frame for drawing to the screen
121        private static JFrame frame;
122
123        // mouse state
124        private static boolean mousePressed = false;
125        private static double mouseX = 0;
126        private static double mouseY = 0;
127
128        // queue of typed key characters
129        private static LinkedList<Character> keysTyped = new LinkedList<>();
130
131        // set of key codes currently pressed down
132        private static TreeSet<Integer> keysDown = new TreeSet<>();
133
134
135        // singleton pattern: client can't instantiate
136        private StdDraw() { }
137
138
139        // static initializer
140        static { init(); }
141
142        /**
143         * Set the window size to the default size 512-by-512 pixels.
144         */
145        public static void setCanvasSize() {
146                setCanvasSize(DEFAULT_SIZE, DEFAULT_SIZE);
147        }
148
149        /**
150         * Set the window size to w-by-h pixels.
151         *
152         * @param w the width as a number of pixels
153         * @param h the height as a number of pixels
154         * @throws java.lang.RuntimeException if the width or height is 0 or negative
155         */
156        public static void setCanvasSize(int w, int h) {
157                if (w < 1 || h < 1) throw new RuntimeException("width and height must be positive");
158                width = w;
159                height = h;
160                init();
161        }
162
163        // init
164        private static void init() {
165                if (frame != null) frame.setVisible(false);
166                frame = new JFrame();
167                offscreenImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
168                onscreenImage  = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
169                offscreen = offscreenImage.createGraphics();
170                onscreen  = onscreenImage.createGraphics();
171                setXscale();
172                setYscale();
173                offscreen.setColor(DEFAULT_CLEAR_COLOR);
174                offscreen.fillRect(0, 0, width, height);
175                setPenColor();
176                setPenRadius();
177                setFont();
178                clear();
179
180                // add antialiasing
181                RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
182                                RenderingHints.VALUE_ANTIALIAS_ON);
183                hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
184                offscreen.addRenderingHints(hints);
185
186                // frame stuff
187                ImageIcon icon = new ImageIcon(onscreenImage);
188                JLabel draw = new JLabel(icon);
189
190                draw.addMouseListener(std);
191                draw.addMouseMotionListener(std);
192
193                frame.setContentPane(draw);
194                frame.addKeyListener(std);    // JLabel cannot get keyboard focus
195                frame.setResizable(false);
196                frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);    // closes all windows
197                //frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); // closes only current window
198                frame.setTitle("Standard Draw");
199                frame.setJMenuBar(createMenuBar());
200                frame.pack();
201                frame.requestFocusInWindow();
202                frame.setVisible(true);
203        }
204
205        // create the menu bar (changed to private)
206        @SuppressWarnings("deprecation")
207        private static JMenuBar createMenuBar() {
208                JMenuBar menuBar = new JMenuBar();
209                JMenu menu = new JMenu("File");
210                menuBar.add(menu);
211                JMenuItem menuItem1 = new JMenuItem(" Save...   ");
212                menuItem1.addActionListener(std);
213                menuItem1.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S,
214                                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
215                menu.add(menuItem1);
216                return menuBar;
217        }
218
219
220        /* ***********************************************************************
221         *  User and screen coordinate systems
222         *************************************************************************/
223
224        /**
225         * Set the x-scale to be the default (between 0.0 and 1.0).
226         */
227        public static void setXscale() { setXscale(DEFAULT_XMIN, DEFAULT_XMAX); }
228
229        /**
230         * Set the y-scale to be the default (between 0.0 and 1.0).
231         */
232        public static void setYscale() { setYscale(DEFAULT_YMIN, DEFAULT_YMAX); }
233
234        /**
235         * Set the x-scale (a 10% border is added to the values)
236         * @param min the minimum value of the x-scale
237         * @param max the maximum value of the x-scale
238         */
239        public static void setXscale(double min, double max) {
240                double size = max - min;
241                synchronized (mouseLock) {
242                        xmin = min - BORDER * size;
243                        xmax = max + BORDER * size;
244                }
245        }
246
247        /**
248         * Set the y-scale (a 10% border is added to the values).
249         * @param min the minimum value of the y-scale
250         * @param max the maximum value of the y-scale
251         */
252        public static void setYscale(double min, double max) {
253                double size = max - min;
254                synchronized (mouseLock) {
255                        ymin = min - BORDER * size;
256                        ymax = max + BORDER * size;
257                }
258        }
259
260        /**
261         * Set the x-scale and y-scale (a 10% border is added to the values)
262         * @param min the minimum value of the x- and y-scales
263         * @param max the maximum value of the x- and y-scales
264         */
265        public static void setScale(double min, double max) {
266                double size = max - min;
267                synchronized (mouseLock) {
268                        xmin = min - BORDER * size;
269                        xmax = max + BORDER * size;
270                        ymin = min - BORDER * size;
271                        ymax = max + BORDER * size;
272                }
273        }
274
275        // helper functions that scale from user coordinates to screen coordinates and back
276        private static double  scaleX(double x) { return width  * (x - xmin) / (xmax - xmin); }
277        private static double  scaleY(double y) { return height * (ymax - y) / (ymax - ymin); }
278        private static double factorX(double w) { return w * width  / Math.abs(xmax - xmin);  }
279        private static double factorY(double h) { return h * height / Math.abs(ymax - ymin);  }
280        private static double   userX(double x) { return xmin + x * (xmax - xmin) / width;    }
281        private static double   userY(double y) { return ymax - y * (ymax - ymin) / height;   }
282
283
284        /**
285         * Clear the screen to the default color (white).
286         */
287        public static void clear() { clear(DEFAULT_CLEAR_COLOR); }
288        /**
289         * Clear the screen to the given color.
290         * @param color the Color to make the background
291         */
292        public static void clear(Color color) {
293                offscreen.setColor(color);
294                offscreen.fillRect(0, 0, width, height);
295                offscreen.setColor(penColor);
296                draw();
297        }
298
299        /**
300         * Get the current pen radius.
301         */
302        public static double getPenRadius() { return penRadius; }
303
304        /**
305         * Set the pen size to the default (.002).
306         */
307        public static void setPenRadius() { setPenRadius(DEFAULT_PEN_RADIUS); }
308        /**
309         * Set the radius of the pen to the given size.
310         * @param r the radius of the pen
311         * @throws java.lang.RuntimeException if r is negative
312         */
313        public static void setPenRadius(double r) {
314                if (r < 0) throw new RuntimeException("pen radius must be positive");
315                penRadius = r;
316                float scaledPenRadius = (float) (r * DEFAULT_SIZE);
317                BasicStroke stroke = new BasicStroke(scaledPenRadius, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
318                // BasicStroke stroke = new BasicStroke(scaledPenRadius);
319                offscreen.setStroke(stroke);
320        }
321
322        /**
323         * Get the current pen color.
324         */
325        public static Color getPenColor() { return penColor; }
326
327        /**
328         * Set the pen color to the default color (black).
329         */
330        public static void setPenColor() { setPenColor(DEFAULT_PEN_COLOR); }
331        /**
332         * Set the pen color to the given color. The available pen colors are
333         * BLACK, BLUE, CYAN, DARK_GRAY, GRAY, GREEN, LIGHT_GRAY, MAGENTA,
334         * ORANGE, PINK, RED, WHITE, and YELLOW.
335         * @param color the Color to make the pen
336         */
337        public static void setPenColor(Color color) {
338                penColor = color;
339                offscreen.setColor(penColor);
340        }
341
342        /**
343         * Get the current font.
344         */
345        public static Font getFont() { return font; }
346
347        /**
348         * Set the font to the default font (sans serif, 16 point).
349         */
350        public static void setFont() { setFont(DEFAULT_FONT); }
351
352        /**
353         * Set the font to the given value.
354         * @param f the font to make text
355         */
356        public static void setFont(Font f) { font = f; }
357
358
359        /* ***********************************************************************
360         *  Drawing geometric shapes.
361         *************************************************************************/
362
363        /**
364         * Draw a line from (x0, y0) to (x1, y1).
365         * @param x0 the x-coordinate of the starting point
366         * @param y0 the y-coordinate of the starting point
367         * @param x1 the x-coordinate of the destination point
368         * @param y1 the y-coordinate of the destination point
369         */
370        public static void line(double x0, double y0, double x1, double y1) {
371                offscreen.draw(new Line2D.Double(scaleX(x0), scaleY(y0), scaleX(x1), scaleY(y1)));
372                draw();
373        }
374
375        /**
376         * Draw one pixel at (x, y).
377         * @param x the x-coordinate of the pixel
378         * @param y the y-coordinate of the pixel
379         */
380        private static void pixel(double x, double y) {
381                offscreen.fillRect((int) Math.round(scaleX(x)), (int) Math.round(scaleY(y)), 1, 1);
382        }
383
384        /**
385         * Draw a point at (x, y).
386         * @param x the x-coordinate of the point
387         * @param y the y-coordinate of the point
388         */
389        public static void point(double x, double y) {
390                double xs = scaleX(x);
391                double ys = scaleY(y);
392                double r = penRadius;
393                float scaledPenRadius = (float) (r * DEFAULT_SIZE);
394
395                // double ws = factorX(2*r);
396                // double hs = factorY(2*r);
397                // if (ws <= 1 && hs <= 1) pixel(x, y);
398                if (scaledPenRadius <= 1) pixel(x, y);
399                else offscreen.fill(new Ellipse2D.Double(xs - scaledPenRadius/2, ys - scaledPenRadius/2,
400                                scaledPenRadius, scaledPenRadius));
401                draw();
402        }
403
404        /**
405         * Draw a circle of radius r, centered on (x, y).
406         * @param x the x-coordinate of the center of the circle
407         * @param y the y-coordinate of the center of the circle
408         * @param r the radius of the circle
409         * @throws java.lang.RuntimeException if the radius of the circle is negative
410         */
411        public static void circle(double x, double y, double r) {
412                if (r < 0) throw new RuntimeException("circle radius can't be negative");
413                double xs = scaleX(x);
414                double ys = scaleY(y);
415                double ws = factorX(2*r);
416                double hs = factorY(2*r);
417                if (ws <= 1 && hs <= 1) pixel(x, y);
418                else offscreen.draw(new Ellipse2D.Double(xs - ws/2, ys - hs/2, ws, hs));
419                draw();
420        }
421
422        /**
423         * Draw filled circle of radius r, centered on (x, y).
424         * @param x the x-coordinate of the center of the circle
425         * @param y the y-coordinate of the center of the circle
426         * @param r the radius of the circle
427         * @throws java.lang.RuntimeException if the radius of the circle is negative
428         */
429        public static void filledCircle(double x, double y, double r) {
430                if (r < 0) throw new RuntimeException("circle radius can't be negative");
431                double xs = scaleX(x);
432                double ys = scaleY(y);
433                double ws = factorX(2*r);
434                double hs = factorY(2*r);
435                if (ws <= 1 && hs <= 1) pixel(x, y);
436                else offscreen.fill(new Ellipse2D.Double(xs - ws/2, ys - hs/2, ws, hs));
437                draw();
438        }
439
440
441        /**
442         * Draw an ellipse with given semimajor and semiminor axes, centered on (x, y).
443         * @param x the x-coordinate of the center of the ellipse
444         * @param y the y-coordinate of the center of the ellipse
445         * @param semiMajorAxis is the semimajor axis of the ellipse
446         * @param semiMinorAxis is the semiminor axis of the ellipse
447         * @throws java.lang.RuntimeException if either of the axes are negative
448         */
449        public static void ellipse(double x, double y, double semiMajorAxis, double semiMinorAxis) {
450                if (semiMajorAxis < 0) throw new RuntimeException("ellipse semimajor axis can't be negative");
451                if (semiMinorAxis < 0) throw new RuntimeException("ellipse semiminor axis can't be negative");
452                double xs = scaleX(x);
453                double ys = scaleY(y);
454                double ws = factorX(2*semiMajorAxis);
455                double hs = factorY(2*semiMinorAxis);
456                if (ws <= 1 && hs <= 1) pixel(x, y);
457                else offscreen.draw(new Ellipse2D.Double(xs - ws/2, ys - hs/2, ws, hs));
458                draw();
459        }
460
461        /**
462         * Draw an ellipse with given semimajor and semiminor axes, centered on (x, y).
463         * @param x the x-coordinate of the center of the ellipse
464         * @param y the y-coordinate of the center of the ellipse
465         * @param semiMajorAxis is the semimajor axis of the ellipse
466         * @param semiMinorAxis is the semiminor axis of the ellipse
467         * @throws java.lang.RuntimeException if either of the axes are negative
468         */
469        public static void filledEllipse(double x, double y, double semiMajorAxis, double semiMinorAxis) {
470                if (semiMajorAxis < 0) throw new RuntimeException("ellipse semimajor axis can't be negative");
471                if (semiMinorAxis < 0) throw new RuntimeException("ellipse semiminor axis can't be negative");
472                double xs = scaleX(x);
473                double ys = scaleY(y);
474                double ws = factorX(2*semiMajorAxis);
475                double hs = factorY(2*semiMinorAxis);
476                if (ws <= 1 && hs <= 1) pixel(x, y);
477                else offscreen.fill(new Ellipse2D.Double(xs - ws/2, ys - hs/2, ws, hs));
478                draw();
479        }
480
481
482        /**
483         * Draw an arc of radius r, centered on (x, y), from angle1 to angle2 (in degrees).
484         * @param x the x-coordinate of the center of the circle
485         * @param y the y-coordinate of the center of the circle
486         * @param r the radius of the circle
487         * @param angle1 the starting angle. 0 would mean an arc beginning at 3 o'clock.
488         * @param angle2 the angle at the end of the arc. For example, if
489         *        you want a 90 degree arc, then angle2 should be angle1 + 90.
490         * @throws java.lang.RuntimeException if the radius of the circle is negative
491         */
492        public static void arc(double x, double y, double r, double angle1, double angle2) {
493                if (r < 0) throw new RuntimeException("arc radius can't be negative");
494                while (angle2 < angle1) angle2 += 360;
495                double xs = scaleX(x);
496                double ys = scaleY(y);
497                double ws = factorX(2*r);
498                double hs = factorY(2*r);
499                if (ws <= 1 && hs <= 1) pixel(x, y);
500                else offscreen.draw(new Arc2D.Double(xs - ws/2, ys - hs/2, ws, hs, angle1, angle2 - angle1, Arc2D.OPEN));
501                draw();
502        }
503
504        /**
505         * Draw a square of side length 2r, centered on (x, y).
506         * @param x the x-coordinate of the center of the square
507         * @param y the y-coordinate of the center of the square
508         * @param r radius is half the length of any side of the square
509         * @throws java.lang.RuntimeException if r is negative
510         */
511        public static void square(double x, double y, double r) {
512                if (r < 0) throw new RuntimeException("square side length can't be negative");
513                double xs = scaleX(x);
514                double ys = scaleY(y);
515                double ws = factorX(2*r);
516                double hs = factorY(2*r);
517                if (ws <= 1 && hs <= 1) pixel(x, y);
518                else offscreen.draw(new Rectangle2D.Double(xs - ws/2, ys - hs/2, ws, hs));
519                draw();
520        }
521
522        /**
523         * Draw a filled square of side length 2r, centered on (x, y).
524         * @param x the x-coordinate of the center of the square
525         * @param y the y-coordinate of the center of the square
526         * @param r radius is half the length of any side of the square
527         * @throws java.lang.RuntimeException if r is negative
528         */
529        public static void filledSquare(double x, double y, double r) {
530                if (r < 0) throw new RuntimeException("square side length can't be negative");
531                double xs = scaleX(x);
532                double ys = scaleY(y);
533                double ws = factorX(2*r);
534                double hs = factorY(2*r);
535                if (ws <= 1 && hs <= 1) pixel(x, y);
536                else offscreen.fill(new Rectangle2D.Double(xs - ws/2, ys - hs/2, ws, hs));
537                draw();
538        }
539
540
541        /**
542         * Draw a rectangle of given half width and half height, centered on (x, y).
543         * @param x the x-coordinate of the center of the rectangle
544         * @param y the y-coordinate of the center of the rectangle
545         * @param halfWidth is half the width of the rectangle
546         * @param halfHeight is half the height of the rectangle
547         * @throws java.lang.RuntimeException if halfWidth or halfHeight is negative
548         */
549        public static void rectangle(double x, double y, double halfWidth, double halfHeight) {
550                if (halfWidth  < 0) throw new RuntimeException("half width can't be negative");
551                if (halfHeight < 0) throw new RuntimeException("half height can't be negative");
552                double xs = scaleX(x);
553                double ys = scaleY(y);
554                double ws = factorX(2*halfWidth);
555                double hs = factorY(2*halfHeight);
556                if (ws <= 1 && hs <= 1) pixel(x, y);
557                else offscreen.draw(new Rectangle2D.Double(xs - ws/2, ys - hs/2, ws, hs));
558                draw();
559        }
560
561        /**
562         * Draw a filled rectangle of given half width and half height, centered on (x, y).
563         * @param x the x-coordinate of the center of the rectangle
564         * @param y the y-coordinate of the center of the rectangle
565         * @param halfWidth is half the width of the rectangle
566         * @param halfHeight is half the height of the rectangle
567         * @throws java.lang.RuntimeException if halfWidth or halfHeight is negative
568         */
569        public static void filledRectangle(double x, double y, double halfWidth, double halfHeight) {
570                if (halfWidth  < 0) throw new RuntimeException("half width can't be negative");
571                if (halfHeight < 0) throw new RuntimeException("half height can't be negative");
572                double xs = scaleX(x);
573                double ys = scaleY(y);
574                double ws = factorX(2*halfWidth);
575                double hs = factorY(2*halfHeight);
576                if (ws <= 1 && hs <= 1) pixel(x, y);
577                else offscreen.fill(new Rectangle2D.Double(xs - ws/2, ys - hs/2, ws, hs));
578                draw();
579        }
580
581
582        /**
583         * Draw a polygon with the given (x[i], y[i]) coordinates.
584         * @param x an array of all the x-coordindates of the polygon
585         * @param y an array of all the y-coordindates of the polygon
586         */
587        public static void polygon(double[] x, double[] y) {
588                int N = x.length;
589                GeneralPath path = new GeneralPath();
590                path.moveTo((float) scaleX(x[0]), (float) scaleY(y[0]));
591                for (int i = 0; i < N; i++)
592                        path.lineTo((float) scaleX(x[i]), (float) scaleY(y[i]));
593                path.closePath();
594                offscreen.draw(path);
595                draw();
596        }
597
598        /**
599         * Draw a filled polygon with the given (x[i], y[i]) coordinates.
600         * @param x an array of all the x-coordindates of the polygon
601         * @param y an array of all the y-coordindates of the polygon
602         */
603        public static void filledPolygon(double[] x, double[] y) {
604                int N = x.length;
605                GeneralPath path = new GeneralPath();
606                path.moveTo((float) scaleX(x[0]), (float) scaleY(y[0]));
607                for (int i = 0; i < N; i++)
608                        path.lineTo((float) scaleX(x[i]), (float) scaleY(y[i]));
609                path.closePath();
610                offscreen.fill(path);
611                draw();
612        }
613
614
615
616        /* ***********************************************************************
617         *  Drawing images.
618         *************************************************************************/
619
620        // get an image from the given filename
621        private static Image getImage(String filename) {
622
623                // to read from file
624                ImageIcon icon = new ImageIcon(filename);
625
626                // try to read from URL
627                if ((icon == null) || (icon.getImageLoadStatus() != MediaTracker.COMPLETE)) {
628                        try {
629                                URL url = new URL(filename);
630                                icon = new ImageIcon(url);
631                        } catch (Exception e) { /* not a url */ }
632                }
633
634                // in case file is inside a .jar
635                if ((icon == null) || (icon.getImageLoadStatus() != MediaTracker.COMPLETE)) {
636                        URL url = StdDraw.class.getResource(filename);
637                        if (url == null) throw new RuntimeException("image " + filename + " not found");
638                        icon = new ImageIcon(url);
639                }
640
641                return icon.getImage();
642        }
643
644        /**
645         * Draw picture (gif, jpg, or png) centered on (x, y).
646         * @param x the center x-coordinate of the image
647         * @param y the center y-coordinate of the image
648         * @param s the name of the image/picture, e.g., "ball.gif"
649         * @throws java.lang.RuntimeException if the image is corrupt
650         */
651        public static void picture(double x, double y, String s) {
652                Image image = getImage(s);
653                double xs = scaleX(x);
654                double ys = scaleY(y);
655                int ws = image.getWidth(null);
656                int hs = image.getHeight(null);
657                if (ws < 0 || hs < 0) throw new RuntimeException("image " + s + " is corrupt");
658
659                offscreen.drawImage(image, (int) Math.round(xs - ws/2.0), (int) Math.round(ys - hs/2.0), null);
660                draw();
661        }
662
663        /**
664         * Draw picture (gif, jpg, or png) centered on (x, y),
665         * rotated given number of degrees
666         * @param x the center x-coordinate of the image
667         * @param y the center y-coordinate of the image
668         * @param s the name of the image/picture, e.g., "ball.gif"
669         * @param degrees is the number of degrees to rotate counterclockwise
670         * @throws java.lang.RuntimeException if the image is corrupt
671         */
672        public static void picture(double x, double y, String s, double degrees) {
673                Image image = getImage(s);
674                double xs = scaleX(x);
675                double ys = scaleY(y);
676                int ws = image.getWidth(null);
677                int hs = image.getHeight(null);
678                if (ws < 0 || hs < 0) throw new RuntimeException("image " + s + " is corrupt");
679
680                offscreen.rotate(Math.toRadians(-degrees), xs, ys);
681                offscreen.drawImage(image, (int) Math.round(xs - ws/2.0), (int) Math.round(ys - hs/2.0), null);
682                offscreen.rotate(Math.toRadians(+degrees), xs, ys);
683
684                draw();
685        }
686
687        /**
688         * Draw picture (gif, jpg, or png) centered on (x, y), rescaled to w-by-h.
689         * @param x the center x coordinate of the image
690         * @param y the center y coordinate of the image
691         * @param s the name of the image/picture, e.g., "ball.gif"
692         * @param w the width of the image
693         * @param h the height of the image
694         * @throws java.lang.RuntimeException if the width height are negative
695         * @throws java.lang.RuntimeException if the image is corrupt
696         */
697        public static void picture(double x, double y, String s, double w, double h) {
698                Image image = getImage(s);
699                double xs = scaleX(x);
700                double ys = scaleY(y);
701                if (w < 0) throw new RuntimeException("width is negative: " + w);
702                if (h < 0) throw new RuntimeException("height is negative: " + h);
703                double ws = factorX(w);
704                double hs = factorY(h);
705                if (ws < 0 || hs < 0) throw new RuntimeException("image " + s + " is corrupt");
706                if (ws <= 1 && hs <= 1) pixel(x, y);
707                else {
708                        offscreen.drawImage(image, (int) Math.round(xs - ws/2.0),
709                                        (int) Math.round(ys - hs/2.0),
710                                        (int) Math.round(ws),
711                                        (int) Math.round(hs), null);
712                }
713                draw();
714        }
715
716
717        /**
718         * Draw picture (gif, jpg, or png) centered on (x, y), rotated
719         * given number of degrees, rescaled to w-by-h.
720         * @param x the center x-coordinate of the image
721         * @param y the center y-coordinate of the image
722         * @param s the name of the image/picture, e.g., "ball.gif"
723         * @param w the width of the image
724         * @param h the height of the image
725         * @param degrees is the number of degrees to rotate counterclockwise
726         * @throws java.lang.RuntimeException if the image is corrupt
727         */
728        public static void picture(double x, double y, String s, double w, double h, double degrees) {
729                Image image = getImage(s);
730                double xs = scaleX(x);
731                double ys = scaleY(y);
732                double ws = factorX(w);
733                double hs = factorY(h);
734                if (ws < 0 || hs < 0) throw new RuntimeException("image " + s + " is corrupt");
735                if (ws <= 1 && hs <= 1) pixel(x, y);
736
737                offscreen.rotate(Math.toRadians(-degrees), xs, ys);
738                offscreen.drawImage(image, (int) Math.round(xs - ws/2.0),
739                                (int) Math.round(ys - hs/2.0),
740                                (int) Math.round(ws),
741                                (int) Math.round(hs), null);
742                offscreen.rotate(Math.toRadians(+degrees), xs, ys);
743
744                draw();
745        }
746
747
748        /* ***********************************************************************
749         *  Drawing text.
750         *************************************************************************/
751
752        /**
753         * Write the given text string in the current font, centered on (x, y).
754         * @param x the center x-coordinate of the text
755         * @param y the center y-coordinate of the text
756         * @param s the text
757         */
758        public static void text(double x, double y, String s) {
759                offscreen.setFont(font);
760                FontMetrics metrics = offscreen.getFontMetrics();
761                double xs = scaleX(x);
762                double ys = scaleY(y);
763                int ws = metrics.stringWidth(s);
764                int hs = metrics.getDescent();
765                offscreen.drawString(s, (float) (xs - ws/2.0), (float) (ys + hs));
766                draw();
767        }
768
769        /**
770         * Write the given text string in the current font, centered on (x, y) and
771         * rotated by the specified number of degrees
772         * @param x the center x-coordinate of the text
773         * @param y the center y-coordinate of the text
774         * @param s the text
775         * @param degrees is the number of degrees to rotate counterclockwise
776         */
777        public static void text(double x, double y, String s, double degrees) {
778                double xs = scaleX(x);
779                double ys = scaleY(y);
780                offscreen.rotate(Math.toRadians(-degrees), xs, ys);
781                text(x, y, s);
782                offscreen.rotate(Math.toRadians(+degrees), xs, ys);
783        }
784
785
786        /**
787         * Write the given text string in the current font, left-aligned at (x, y).
788         * @param x the x-coordinate of the text
789         * @param y the y-coordinate of the text
790         * @param s the text
791         */
792        public static void textLeft(double x, double y, String s) {
793                offscreen.setFont(font);
794                FontMetrics metrics = offscreen.getFontMetrics();
795                double xs = scaleX(x);
796                double ys = scaleY(y);
797                int hs = metrics.getDescent();
798                offscreen.drawString(s, (float) (xs), (float) (ys + hs));
799                draw();
800        }
801
802        /**
803         * Write the given text string in the current font, right-aligned at (x, y).
804         * @param x the x-coordinate of the text
805         * @param y the y-coordinate of the text
806         * @param s the text
807         */
808        public static void textRight(double x, double y, String s) {
809                offscreen.setFont(font);
810                FontMetrics metrics = offscreen.getFontMetrics();
811                double xs = scaleX(x);
812                double ys = scaleY(y);
813                int ws = metrics.stringWidth(s);
814                int hs = metrics.getDescent();
815                offscreen.drawString(s, (float) (xs - ws), (float) (ys + hs));
816                draw();
817        }
818
819
820
821        /**
822         * Display on screen, pause for t milliseconds, and turn on
823         * <em>animation mode</em>: subsequent calls to
824         * drawing methods such as {@code line()}, {@code circle()}, and {@code square()}
825         * will not be displayed on screen until the next call to {@code show()}.
826         * This is useful for producing animations (clear the screen, draw a bunch of shapes,
827         * display on screen for a fixed amount of time, and repeat). It also speeds up
828         * drawing a huge number of shapes (call {@code show(0)} to defer drawing
829         * on screen, draw the shapes, and call {@code show(0)} to display them all
830         * on screen at once).
831         * @param t number of milliseconds
832         */
833        public static void show(int t) {
834                defer = false;
835                draw();
836                try { Thread.sleep(t); }
837                catch (InterruptedException e) { System.out.println("Error sleeping"); }
838                defer = true;
839        }
840
841        /**
842         * Display on-screen and turn off animation mode:
843         * subsequent calls to
844         * drawing methods such as {@code line()}, {@code circle()}, and {@code square()}
845         * will be displayed on screen when called. This is the default.
846         */
847        public static void show() {
848                defer = false;
849                draw();
850        }
851
852        // draw onscreen if defer is false
853        private static void draw() {
854                if (defer) return;
855                onscreen.drawImage(offscreenImage, 0, 0, null);
856                frame.repaint();
857        }
858
859
860        /* ***********************************************************************
861         *  Save drawing to a file.
862         *************************************************************************/
863
864        /**
865         * Save onscreen image to file - suffix must be png, jpg, or gif.
866         * @param filename the name of the file with one of the required suffixes
867         */
868        public static void save(String filename) {
869                File file = new File(filename);
870                String suffix = filename.substring(filename.lastIndexOf('.') + 1);
871
872                // png files
873                if (suffix.toLowerCase().equals("png")) {
874                        try { ImageIO.write(onscreenImage, suffix, file); }
875                        catch (IOException e) { e.printStackTrace(); }
876                }
877
878                // need to change from ARGB to RGB for jpeg
879                // reference: http://archives.java.sun.com/cgi-bin/wa?A2=ind0404&L=java2d-interest&D=0&P=2727
880                else if (suffix.toLowerCase().equals("jpg")) {
881                        WritableRaster raster = onscreenImage.getRaster();
882                        WritableRaster newRaster;
883                        newRaster = raster.createWritableChild(0, 0, width, height, 0, 0, new int[] {0, 1, 2});
884                        DirectColorModel cm = (DirectColorModel) onscreenImage.getColorModel();
885                        DirectColorModel newCM = new DirectColorModel(cm.getPixelSize(),
886                                        cm.getRedMask(),
887                                        cm.getGreenMask(),
888                                        cm.getBlueMask());
889                        BufferedImage rgbBuffer = new BufferedImage(newCM, newRaster, false,  null);
890                        try { ImageIO.write(rgbBuffer, suffix, file); }
891                        catch (IOException e) { e.printStackTrace(); }
892                }
893
894                else {
895                        System.out.println("Invalid image file type: " + suffix);
896                }
897        }
898
899
900        /**
901         * This method cannot be called directly.
902         */
903        public void actionPerformed(ActionEvent e) {
904                FileDialog chooser = new FileDialog(StdDraw.frame, "Use a .png or .jpg extension", FileDialog.SAVE);
905                chooser.setVisible(true);
906                String filename = chooser.getFile();
907                if (filename != null) {
908                        StdDraw.save(chooser.getDirectory() + File.separator + chooser.getFile());
909                }
910        }
911
912
913        /* ***********************************************************************
914         *  Mouse interactions.
915         *************************************************************************/
916
917        /**
918         * Is the mouse being pressed?
919         * @return true or false
920         */
921        public static boolean mousePressed() {
922                synchronized (mouseLock) {
923                        return mousePressed;
924                }
925        }
926
927        /**
928         * What is the x-coordinate of the mouse?
929         * @return the value of the x-coordinate of the mouse
930         */
931        public static double mouseX() {
932                synchronized (mouseLock) {
933                        return mouseX;
934                }
935        }
936
937        /**
938         * What is the y-coordinate of the mouse?
939         * @return the value of the y-coordinate of the mouse
940         */
941        public static double mouseY() {
942                synchronized (mouseLock) {
943                        return mouseY;
944                }
945        }
946
947
948        /**
949         * This method cannot be called directly.
950         */
951        public void mouseClicked(MouseEvent e) { }
952
953        /**
954         * This method cannot be called directly.
955         */
956        public void mouseEntered(MouseEvent e) { }
957
958        /**
959         * This method cannot be called directly.
960         */
961        public void mouseExited(MouseEvent e) { }
962
963        /**
964         * This method cannot be called directly.
965         */
966        public void mousePressed(MouseEvent e) {
967                synchronized (mouseLock) {
968                        mouseX = StdDraw.userX(e.getX());
969                        mouseY = StdDraw.userY(e.getY());
970                        mousePressed = true;
971                }
972        }
973
974        /**
975         * This method cannot be called directly.
976         */
977        public void mouseReleased(MouseEvent e) {
978                synchronized (mouseLock) {
979                        mousePressed = false;
980                }
981        }
982
983        /**
984         * This method cannot be called directly.
985         */
986        public void mouseDragged(MouseEvent e)  {
987                synchronized (mouseLock) {
988                        mouseX = StdDraw.userX(e.getX());
989                        mouseY = StdDraw.userY(e.getY());
990                }
991        }
992
993        /**
994         * This method cannot be called directly.
995         */
996        public void mouseMoved(MouseEvent e) {
997                synchronized (mouseLock) {
998                        mouseX = StdDraw.userX(e.getX());
999                        mouseY = StdDraw.userY(e.getY());
1000                }
1001        }
1002
1003
1004        /* ***********************************************************************
1005         *  Keyboard interactions.
1006         *************************************************************************/
1007
1008        /**
1009         * Has the user typed a key?
1010         * @return true if the user has typed a key, false otherwise
1011         */
1012        public static boolean hasNextKeyTyped() {
1013                synchronized (keyLock) {
1014                        return !keysTyped.isEmpty();
1015                }
1016        }
1017
1018        /**
1019         * What is the next key that was typed by the user? This method returns
1020         * a Unicode character corresponding to the key typed (such as 'a' or 'A').
1021         * It cannot identify action keys (such as F1
1022         * and arrow keys) or modifier keys (such as control).
1023         * @return the next Unicode key typed
1024         */
1025        public static char nextKeyTyped() {
1026                synchronized (keyLock) {
1027                        return keysTyped.removeLast();
1028                }
1029        }
1030
1031        /**
1032         * Is the keycode currently being pressed? This method takes as an argument
1033         * the keycode (corresponding to a physical key). It can handle action keys
1034         * (such as F1 and arrow keys) and modifier keys (such as shift and control).
1035         * See <a href = "http://download.oracle.com/javase/6/docs/api/java/awt/event/KeyEvent.html">KeyEvent.java</a>
1036         * for a description of key codes.
1037         * @return true if keycode is currently being pressed, false otherwise
1038         */
1039        public static boolean isKeyPressed(int keycode) {
1040                synchronized (keyLock) {
1041                        return keysDown.contains(keycode);
1042                }
1043        }
1044
1045
1046        /**
1047         * This method cannot be called directly.
1048         */
1049        public void keyTyped(KeyEvent e) {
1050                synchronized (keyLock) {
1051                        keysTyped.addFirst(e.getKeyChar());
1052                }
1053        }
1054
1055        /**
1056         * This method cannot be called directly.
1057         */
1058        public void keyPressed(KeyEvent e) {
1059                synchronized (keyLock) {
1060                        keysDown.add(e.getKeyCode());
1061                }
1062        }
1063
1064        /**
1065         * This method cannot be called directly.
1066         */
1067        public void keyReleased(KeyEvent e) {
1068                synchronized (keyLock) {
1069                        keysDown.remove(e.getKeyCode());
1070                }
1071        }
1072
1073
1074
1075
1076        /**
1077         * Test client.
1078         */
1079        public static void main(String[] args) {
1080                StdDraw.square(.2, .8, .1);
1081                StdDraw.filledSquare(.8, .8, .2);
1082                StdDraw.circle(.8, .2, .2);
1083
1084                StdDraw.setPenColor(StdDraw.BOOK_RED);
1085                StdDraw.setPenRadius(.02);
1086                StdDraw.arc(.8, .2, .1, 200, 45);
1087
1088                // draw a blue diamond
1089                StdDraw.setPenRadius();
1090                StdDraw.setPenColor(StdDraw.BOOK_BLUE);
1091                double[] x = { .1, .2, .3, .2 };
1092                double[] y = { .2, .3, .2, .1 };
1093                StdDraw.filledPolygon(x, y);
1094
1095                // text
1096                StdDraw.setPenColor(StdDraw.BLACK);
1097                StdDraw.text(0.2, 0.5, "black text");
1098                StdDraw.setPenColor(StdDraw.WHITE);
1099                StdDraw.text(0.8, 0.8, "white text");
1100        }
1101
1102}