Java Drawing in 2D Animations with Timer Jan SmrcinaReferences: Slides with help from Sun’s Java Tutorials Rick Snodgrass’ Slides on Basic and Advanced Swing http://courses.coreservlets.com/Course-Materials/pdf/java5/12-Java-2D.pdf Drawing Basics A simple two-dimensional coordinate system exists for each graphics context or drawing surface such as a JPanel Points on the coordinate system represent single pixels (no world coordinates exist) Top left corner of the area is coordinate <0, 0> A drawing surface has a width and height example: JPanel has getWidth() and getHeight()methods Anything drawn outside of that area is not visible JComponent To begin drawing we first need a class which extends JComponent. For this we can use JPanel. Once we subclass JPanel we can override the paintComponent() method to specify what we want the panel to paint when repainting. When painting we paint to a Graphics context (which is given to us as an argument to paintComponent). This will be supplied when the method is called but this means that… paintComponent(Graphics g) paintComponent() is the method which is called when repainting a Component. It should never be called explicitly, but instead repaint() should be invoked which will then call paintComponent()on the approriate Components. When we override paintComponent()the first line should be super.paintComponent(g)which will clear the panel for drawing. Graphics vs. Graphics2D We are passed a Graphics object to paintComponent() which we paint to. We can always cast the object passed to a Graphics2D which has much more functionality. Why didn’t Java just pass Graphics2D in? Legacy support Cast: public void paintComponent(Graphics g) { // Clear the Component so we can draw to a fresh canvas super.paintComponent(g); // Cast g to a Graphics2D object Graphics2D g2 = (Graphics2D)g; } Now for some Drawing… Now that we have our Graphics2D object we can draw things to the graphics context which will show up on our panel: public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D)g; g2.drawString("Draw a string to the context...", 20, 20); } Note: When drawing strings the coordinates refer to the BOTTOM left corner. Pulling It Together Now that we have a JPanel with an overridden paintComponent()we can add this JPanel to a JFrame and display it to our user. But drawing is about more than just strings, and Java gives us lots of additional drawing features. Note: A common pitfall is to try to call paintComponent() explicitly. Make sure never to do this, always use the repaint() function! Using Java Geometry Classes within java.awt.geom: AffineTransform, Arc2D, Arc2D.Double, Arc2D.Float, Area, CubicCurve2D, CubicCurve2D.Double, CubicCurve2D.Float, Dimension2D, Ellipse2D,Ellipse2D.Double, Ellipse2D.Float, FlatteningPathIterator, GeneralPath, Line2D, Line2D.Double, Line2D.Float, Path2D, Path2D.Double, Path2D.Float, Point2D, Point2D.Double, Point2D.Float, QuadCurve2D, QuadCurve2D.Double, QuadCurve2D.Float, Rectangle2D, Rectangle2D.Double, Rectangle2D.Float, RectangularShape, RoundRectangle2D, RoundRectangle2D.Double, RoundRectangle2D.Float What are these useful for? Let’s look at a Code example! Using Java Geometry (cont.) public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D)g; // xcoord, ycoord, width, height Rectangle2D body = new Rectangle2D.Double(30.0, 70.0, 200.0, 50.0); g2.draw(body); } Note: For all other shapes the coordinates refer to the TOP left corner. Note: Rectangle2D.Double means that Double is an inner class contained in Rectangle2D. The difference between g2.draw() and g2.fill() is that draw will draw an outline of the object while fill will fill in the object. Until now we have been drawing using a basic penstroke (just a plain black line). Now that we know how to draw shapes we can start messing with the pen. Pen Styles Let’s look at some Pen Styles that we can set: // Basic functionality // Set the Pen color or pattern (Let’s focus on color) g2d.setPaint(fillColorOrPattern); // Set the Stroke style g2d.setStroke(penThicknessOrPattern); // Set Alpha (transparency) g2d.setComposite(someAlphaComposite); g2d.setFont(someFont); // More advanced functionality // Translate to a different point g2d.translate(...); // Rotate around our origin g2d.rotate(...); // Scale everything we draw g2d.scale(...); // Shear what we draw g2d.shear(...); Color To begin modifying the pen we first need to understand color. The Color class in Java allows us to define and manage the color in which shapes are drawn. Colors are defined by their RGB (Red, Green, Blue) values (and possibly an alpha value). Any color can be expressed this way (i.e. Conifer is defined as (140, 225, 65)): The way to tell Java to use a particular color when drawing is by invoking g2.setPaint(Color c) or g2.setColor(Color c) . These two are equivalent. Color (cont.) There are some predefined colors in Java which can be accessed through the Color class (i.e. Color.BLUE is blue). Additionally we can define our own colors through the Color constructor: Color conifer = new Color(140, 225, 65); The values that go into the constructor are 1 byte large (means that the min is 0 and the max is 255). This allows Java to store a color into 4 bytes (the size of an int) including its alpha value (transparency, more on that later). Strokes g2.setStroke(…) allows us to create different strokes and create effects (such as a dotted line). // 30 pxl line, 10 pxl gap, 10 pxl line, 10 pxl gap float[] dashPattern = {30.0f , 10.0f , 10.0f, 10.0f}; // float width, int cap, int join, float miterlimit, float[] dash, // float dash_phase g2d.setStroke(new BasicStroke(8, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10, dashPattern, 0)); g2d.draw(getCircle()); Note: Assume getCircle() returns a geometric circle. What is a cap or join? Cap and Join JOIN_MITER – Extend outside edges of lines until they meet This is the default JOIN_BEVEL – Connect outside corners of outlines with straight line JOIN_ROUND – Round off corner with a circle that has diameter equal to the pen width CAP_SQUARE – Make a square cap that extends past the end point by half the pen width This is the default CAP_BUTT – Cut off segment exactly at end point Use this one for dashed lines. CAP_ROUND – Make a circle centered on the end point. Use a diameter equal to the pen width. What does this all mean!? Let’s look at a graphical example… Cap and Join (cont.) Alpha (Transparency) Concept: We want to assign transparency (alpha) values to drawing operations so that the underlying graphics partially shows through when you draw shapes or images. Execution: Create an AlphaComposite object using AlphaComposite.getInstance (Singleton anyone?) with a mixing rule. There are 12 built-in mixing rules but we only care about AlphaComposite.SRC_OVER. See the AlphaComposite API for more details. Alpha values range from 0.0f to 1.0f, completely transparent and completely opaque respectively. Finally pass the AlphaCoposite object to g2.setComposite(…) so that it will be used in our rendering. Let’s see an example… Alpha (Transparency, cont.) Code: public void paintComponent(Graphics g) { Composite originalComposite = g2d.getComposite(); g2d.setPaint(Color.BLUE); g2d.fill(blueSquare); AlphaComposite alphaComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f); g2d.setComposite(alphaComposite); g2d.setPaint(Color.RED); g2d.fill(redSquare); g2d.setComposite(originalComposite); } Note: assume blueSquare and redSquare exist with set positions and sizes Fonts A Font object is constructed with 3 arguments to indicate the logical font names such as "SansSerif" style such as Font.PLAIN and Font.BOLD font size Once we have constructed a font we can tell Java to use it using g2.setFont(Font f). Example: Font aFont = new Font("SansSerif", Font.BOLD, 16); g2.setFont(aFont); Note: Avoid font names like "AvantGuard" or "Book Antiqua" since they may not be installed Instead, use logical font names mapped to fonts actually installed. On windows, SansSerif is Arial. Examples: "SansSerif" "Serif" "Monospaced" "Dialog" "DialogInput" Fonts (cont.) Let’s say we want to use a cool font but we aren’t sure if it’s installed. We can ask the environment for all of the installed Fonts (this is slow so only do it once, i.e. not in paintComponent()). We can then safely use the font if we know it is present. Example: GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); String[] fontNames = env.getAvailableFontFamilyNames(); System.out.println("Available Fonts:"); for(int i = 0; i < fontNames.length; i++) System.out.println(" " + fontNames[i]); What happens if we try to use a Font that isn’t installed? Java will use one of the default fonts Advanced Functionality Most of the advanced functionalities shown earlier (translate, rotate, shear, etc.) are beyond the scope of this class. We will discuss affineTransformations so that you can rotate your images in your program (if you need to). Before we can talk about rotating images we need to know how to instantiate and draw them! Images and BufferedImage The BufferedImage subclass describes an Image with an accessible buffer of image data. BufferedImages are what we will use to draw images to the screen. Once we have an image, painting it to our canvas is easy: public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D)g; // Image img, int x, int y, ImageObserver observer g2.drawImage(image, 100, 100, null); } Note: Assume image is defined. There are many overloaded definitions of drawImage, you can find them all in the API Now we know how to paint an image, but we don’t have an image! Drawing to a BufferedImage Let’s say we are writing a JPaint program which will allow us to paint images, much like MS Paint, and then save them to the disk. How could we do this? We create a blank BufferedImage, paint to it, and then write it to the disk. Example: // Construct a blank BufferedImage (width, height, type of image) BufferedImage bufferedImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); // Create a Graphics object for this image Graphics2D g2dImg = bufferedImage.createGraphics(); // Paint whatever we want to it (use g2dImg as you would g2 in paintComponent) g2dImg.drawString(“String on my image”, 100, 100); Now we want to write our BufferedImage to disk. How can we achieve this? ImageIO Java provides a library for reading and writing Images from and to the hard disk so that we don’t have to deal with coding it (becomes very complex when dealing with multiple types such as PNG and JPEG). There are methods for reading and writing images to a file. Reading an image from the disk: BufferedImage img = ImageIO.read(new File(“myimg.png”)); Writing an image to disk: try { // Assume image is from our previous slide ImageIO.write(image, "png", outputfile); } catch (IOException e) { ... } Animation At its simplest, animation is the time-based alteration of graphical objects through different states, locations, sizes and orientations. Most animation consists of cycling through a pre-made set of bitmaps at a certain rate. In general a rate of 24 images (frames) per second is used in film, but many video games do well over this. Note that most displays only support around 60 refreshes per second. Advanced Swing F2-24 Animation: Timing Since code execution time may vary (even on the same machine), animations should take into account how much time has passed from the last refresh to calculate the new values of an animation. When doing animation – note that System.currentTimeMillis() usually has a resolution of 16 milliseconds. System.nanoTime()is more precise. (But not to the precision of one nanosecond!) Advanced Swing F2-25 Animation: Simple Timer Example Here we set a timer to wake up every 10 milliseconds, update a count, and request a repaint. timer = new javax.swing.Timer(10, new ActionListener() { public void actionPerformed(ActionEvent evt) { count++; // count should be volatile! repaint(); if (count == 100) { timer.stop(); } }}); // The timer is started when animation is started // The code in paintComponent() uses the count to calculate // how far along the animation is. public void paintComponent(Graphics g) { ... double percentage = (count / (double) 100); int h1 = (int) (unitHeight * percentage); } Animation: Double Buffering Double buffering is the mechanism of using a second bitmap (or buffer) which receives all of the updates to a window during an update. Once all of the objects of a window have been drawn, then the bitmap is copied to the primary bitmap seen on the screen. This prevents the flashing from appearing to the user that occurs when multiple items are being drawn to the screen. Animation: Double Buffering In Swing Swing now (as of Java 6) provides true double buffering. Previously, application programmers had to implement double buffering themselves. The no-arg constructor of JPanel returns a JPanel with a double buffer (and a flow layout) by default. "...uses additional memory space to achieve fast, flicker-free updates." Animation: Double Buffering In Swing Each box drawn Backing Bitmap (not visible to user) Window Bitmap (visible to user) Drawn 1st Drawn 2nd Drawn 3rd Drawn 4th in order might be detectable by user. The entire backing bitmap is copied at once to the visible bitmap, avoiding any flicker. Animation: Coalescence Of Requests Swing will coalesce (or merge) a number of repaint requests into one request if requests occur faster than they can be executed. Caveat to Animated GIF’s Using animated GIF’s in your projects is okay, but there is a better way to animate things. Problems with animated GIF’s: The timings between frames are set in stone and cannot be controlled by the programmer. During the first animation of the GIF it will appear that your image is not loaded since the GIF loads each frame as it is displayed (looks ugly, but can be avoided using image loaders) Programmers also can’t control which frame to display or start/stop on since the GIF just keeps animating. Instead of Animated GIF’s, most 2D games will use what is called a SpriteSheet and then animate using a Timer or Thread. Sprite Sheets A sprite sheet is a depiction of various sprites arranged in one image, detailing the exact frames of animation for each character or object by way of layout. The programmer can then control how fast frames switch and which frame to display. Additionally the whole sheet is loaded at once as a single image and won’t cause a loading glitch. In Java we can use the BufferedImage method: getSubimage(int x, int y, int w, int h) Returns a subimage defined by a specified rectangular region. Basic Affine Transformations An affine transformation is a transformation which is a combination of single transformations such as translation or rotation or reflection on an axis What does this mean for us? It means we can rotate our images with relative ease! More complex affine transformations won’t be discussed here but there are tutorials widely available on the all-powerful internet. Here we finally get to see a functionality only available in Graphics2D: drawImage(Image img, AffineTransform xform, ImageObserver obs) Description: Renders an image, applying a transform from image space into user space before drawing. Note: When rotating, remember that you are rotating around the your origin, so if you don’t set up your transformation correctly it will rotate your image wrong! Let’s see an example! Basic Affine Transformations (cont.) public void paintComponent(Graphics g) { Graphics2D g2d = (Graphics2D)g; AffineTransform at = new AffineTransform(); // rotate 45 degrees around image center at.rotate(45.0 * Math.PI / 180.0, image.getWidth() / 2.0, image.getHeight() / 2.0); //draw the image using the AffineTransform g2d.drawImage(image, at, null); } Assume image is defined. Why do we need to specify the second and third arguments to rotate? This specifies our image center as the point that we are rotating around, otherwise we rotate around 0,0 which isn’t what we intended! Wrap-up Questions, Comments, Concerns, etc? Sprite Sheet Example (if time permits)