The pre-requisite knowledge needed to for game programming includes:
- OOP, in particular the concepts of inheritance and polymorphism for designing classes.
- GUI and custom graphics programming (
javax.swing
). - Event-handling, in particular mouse-event and key-event handling (
java.awt.event
). - Graphics programming using Java 2D (
java.awt.geom
). - Paying sounds (
javax.sound
). - Basic knowledge on I/O, multi-threading for starting the game thread, and timing control.
- Transformation, collision detection and reaction algorithms.
Advanced Knowledge:
- JOGL (Java Bindings to OpenGL), or Java3D for 3D graphics.
- JOAL (Java Bindings to OpenAL) for advanced sound.
Revisit java.awt.Graphics for Custom Drawing
Read: "Custom Graphics" chapter.
The java.awt.Graphics
class is used for custom painting. It manages the graphics context (such as color, font and clip area) and provides methods for rendering of three types of graphical objects:
- Texts: via
drawString()
. - Vector-graphic primitives and shapes: via
drawXxx()
andfillXxx()
forLine
,PolyLine
,Oval
,Rect
,RoundRect
,3DRect
, andArc
. - Bitmap images: via
drawImage()
.
The Graphics
class also allows you to get/set the attributes of the graphics context:
- Font (
setFont()
,getFont()
) - Color (
setColor()
,getColor()
) - Display clipping area (
getClip()
,getClipBounds()
,setClip()
)
It is important to take note that the graphics co-ordinates system is "inverted", as illustrated.
The java.awt.Graphics
class is, however, limited in its functions and capabilities. It supports mainly straight-line segments. java.awt.Graphics2D
(of the Java 2D API) greatly extends the functions and capabilities of the Graphics
class.
Template for Custom Drawing
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
import java.awt.*; import java.awt.event.*; import javax.swing.*; // Swing Program Template @SuppressWarnings("serial") public class SwingTemplateJPanel extends JPanel { // Name-constants public static final int CANVAS_WIDTH = 640; public static final int CANVAS_HEIGHT = 480; public static final String TITLE = "...Title..."; // ...... // private variables of GUI components // ...... /** Constructor to setup the GUI components */ public SwingTemplateJPanel() { setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT)); // "this" JPanel container sets layout // setLayout(new ....Layout()); // Allocate the UI components // ..... // "this" JPanel adds components // add(....) // Source object adds listener // ..... } /** Custom painting codes on this JPanel */ @Override public void paintComponent(Graphics g) { super.paintComponent(g); // paint background setBackground(Color.BLACK); // Your custom painting codes // ...... } /** The entry main() method */ public static void main(String[] args) { // Run GUI codes in the Event-Dispatching thread for thread safety SwingUtilities.invokeLater(new Runnable() { public void run() { JFrame frame = new JFrame(TITLE); frame.setContentPane(new SwingTemplateJPanel()); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.pack(); // "this" JFrame packs its components frame.setLocationRelativeTo(null); // center the application window frame.setVisible(true); // show it } }); } } |
Dissecting the Program
- The custom drawing is done by extending a
JPanel
and overrides thepaintComponent()
method. - The size of the drawing panel is set via the
setPreferredSize()
. - The
JFrame
does not set its size, but packs its components by invokingpack()
. - In the
main()
, the GUI construction is carried out in the event-dispatch thread to ensure thread-safety.
You can run the above class as an applet, by providing a main applet class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import javax.swing.*; /** Swing Program Template for running as Applet */ @SuppressWarnings("serial") public class SwingTemplateApplet extends JApplet { /** init() to setup the UI components */ @Override public void init() { // Run GUI codes in the Event-Dispatching thread for thread safety try { SwingUtilities.invokeAndWait(new Runnable() { // Applet uses invokeAndWait() @Override public void run() { // Set the content-pane of the JApplet to an instance of main JPanel setContentPane(new SwingTemplateJPanel()); } }); } catch (Exception e) { e.printStackTrace(); } } } |
Java 2D API & Graphics2D
Reference: Java Tutorial's "2D Graphics" @ http://docs.oracle.com/javase/tutorial/2d/TOC.html.
Java AWT API has been around since JDK 1.1. Java 2D API is part of Java Foundation Class (JFC), similar to Swing, and was introduced in JDK 1.2. It provides more capabilities for graphics programming. Java 2D spans many packages: java.awt
, java.awt.image
, java.awt.color
, java.awt.font
, java.awt.geom
, java.awt.print
, and java.awt.image.renderable
.
java.awt.Graphics2D
The core class in Java2D is the java.awt.Graphics2D
. Graphics2D
is a subclass of java.awt.Graphics
, which extends the support of the legacy
Graphics
class in rendering three groups of objects: text, vector-graphics and bitmap images. It also supports more attributes that affect the rendering, e.g.,
- Transform attribute (translation, rotation, scaling and shearing).
- Pen attribute (outline of a shape) and Stroke attribute (point-size, dashing-pattern, end-cap and join decorations).
- Fill attribute (interior of a shape) and Paint attribute (fill shapes with solid colors, gradients, and patterns).
- Composite attribute (for overlapping shapes).
- Clip attribute (display area).
- Font attribute.
Graphic2D
is designed as a subclass of Graphics
. To use Graphics2D
context, downcast the Graphics
handle g
to Graphics2D
in painting methods such as paintComponent()
. This works because the graphics subsystem in fact passes a Graphics2D
object as the argument when calling back the painting methods. For example,
@Override public void paintComponent(Graphics g) { // graphics subsystem passes a Graphic2D subclass object as argument super.paintComponent(g); // paint parent's background Graphics2D g2d = (Graphics2D) g; // downcast the Graphics object back to Graphics2D // Perform custom drawing using g2d handle ...... }
Besides the drawXxx()
and fillXxx()
, Graphics2D
provides more general drawing methods which operates on Shape
interface.
public abstract void draw(Shape s) public abstract void fill(Shape s) public abstract void clip(Shape s)
Affine Transform (java.awt.geom.AffineTransform)
Transform is key in game programming and animation!
An affine transform is a linear transform such as translation, rotation, scaling, or shearing in which a straight line remains straight and parallel lines remain parallel after the transformation. It can be represented using the following 3x3 matrix operation:
[ x' ] [ m00 m01 m02 ] [ x ] [ m00x + m01y + m02 ] [ y' ] = [ m10 m11 m12 ] [ y ] = [ m10x + m11y + m12 ] [ 1 ] [ 0 0 1 ] [ 1 ] [ 1 ]
Affine transform is supported via the java.awt.geom.AffineTransform
class. The Graphics2D
context maintains an AffineTransform
attribute, and provides methods to change the transform attributes:
// in class java.awt.Graphics2D public abstract void setTransform(AffineTransform at); // overwrites the current Transform in the Graphics2D context public abstract void translate(double tx, double ty); // concatenates the current Transform with a translation transform public abstract void rotate(double theta); // concatenates the current Transform with a rotation transform public abstract void scale(double scaleX, double scaleY) // concatenates the current Transform with a scaling transform public abstract void shear(double shearX, double shearY) // concatenates the current Transform with a shearing transform
Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
import java.awt.*; import java.awt.geom.AffineTransform; import javax.swing.*; /** Test applying affine transform on vector graphics */ @SuppressWarnings("serial") public class AffineTransformDemo extends JPanel { // Named-constants for dimensions public static final int CANVAS_WIDTH = 640; public static final int CANVAS_HEIGHT = 480; public static final String TITLE = "Affine Transform Demo"; // Define an arrow shape using a polygon centered at (0, 0) int[] polygonXs = { -20, 0, +20, 0}; int[] polygonYs = { 20, 10, 20, -20}; Shape shape = new Polygon(polygonXs, polygonYs, polygonXs.length); double x = 50.0, y = 50.0; // (x, y) position of this Shape /** Constructor to set up the GUI components */ public AffineTransformDemo() { setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT)); } /** Custom painting codes on this JPanel */ @Override public void paintComponent(Graphics g) { super.paintComponent(g); // paint background setBackground(Color.WHITE); Graphics2D g2d = (Graphics2D)g; // Save the current transform of the graphics contexts. AffineTransform saveTransform = g2d.getTransform(); // Create a identity affine transform, and apply to the Graphics2D context AffineTransform identity = new AffineTransform(); g2d.setTransform(identity); // Paint Shape (with identity transform), centered at (0, 0) as defined. g2d.setColor(Color.GREEN); g2d.fill(shape); // Translate to the initial (x, y) position, scale, and paint g2d.translate(x, y); g2d.scale(1.2, 1.2); g2d.fill(shape); // Try more transforms for (int i = 0; i < 5; ++i) { g2d.translate(50.0, 5.0); // translates by (50, 5) g2d.setColor(Color.BLUE); g2d.fill(shape); g2d.rotate(Math.toRadians(15.0)); // rotates about transformed origin g2d.setColor(Color.RED); g2d.fill(shape); } // Restore original transform before returning g2d.setTransform(saveTransform); } /** The Entry main method */ public static void main(String[] args) { // Run the GUI codes on the Event-Dispatching thread for thread safety SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JFrame frame = new JFrame(TITLE); frame.setContentPane(new AffineTransformDemo()); frame.pack(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); // center the application window frame.setVisible(true); } }); } } |
These are the steps for carrying out affine transform with a Graphics2D
context:
- Override the
paintComponent(Graphics)
method of the custom drawingJPanel
. Downcast theGraphics
object intoGraphics2D
.@Override public void paintComponent(Graphics g) { Graphics2D g2d = (Graphics2D) g; ...... }
- Prepare a
Shape
(such asPolygon
,Rectangle2D, Rectangle
). The original shape shall center at (0, 0) to simplify rotation. - Save the current transform associated with this
Graphics2D
context, and restore the saved transform before exiting the method.AffineTransform saveTransform = g2d.getTransform(); // save .... g2d.setTransform(saveTransform); // restore
- Allocate a new
AffineTransform
. Initialize to the default identity transform. Apply it to the currentGraphics2D
context. You can then use theGraphics2D
context to perform translation, rotation, scaling and shearing.AffineTransform identity = new AffineTransform(); // an identity transform g2d.setTransform(identity); // overwrites the transform associated with this Graphics2D context g2d.translate(x, y); // translates from (0, 0) to the current (x, y) position g2d.scale(scaleX, scaleY); // scaling g2d.rotate(angle); // rotation clockwise about (0, 0), by angle (in radians) g2d.shear(shearX, shearY); // shearing
Take note that successive transforms are concatenated, until it is reset (to the identity transform) or overwritten.
In the above example, the Polygon
, which is originally centered at (0, 0) (shown in green), is first translated to (50, 50) and scaled up by 1.2 (in green). A loop of 5-iterations is applied to translate by (50, 5) (in blue) and rotate by 15 degrees about (0, 0) (in red). Observe that after each transform, the axes and origin are transformed accordingly. This is especially noticeable for rotation, as the axes are no longer parallel and perpendicular to the screen and its origin is shifted as well. The origin of the axes is set to (0, 0) by the identity transform.
Rotation
The result of rotation depends on the angle rotated, as well as its rotation center. Two methods are provided:
public abstract void rotate(theta); public abstract void rotate(theta, anchorX, anchorY)
The first method rotates about the origin (0, 0), while the second method rotate about (anchorX, anchorY), without affecting the origin after the rotation. Take note that after each rotation, the coordinates is rotated as well. Rotation can be made simpler, by center the shape at (0, 0) as shown in the above example.
Geometric Primitives and Shapes
Java 2D's primitives include:
- point (
Point2D
) - line (
Line2D
) - rectangular shapes (
Rectangle2D
,RoundRectangle2D
,,Ellipse2D
,Arc2D
,Dimension2D
) - quadratic and cubic curves with control points (
QuadCurve2D
,CubicCurve2D
) - arbitrary shapes (
Path2D
).
[TODO] Class diagram
The Xxx2D
classes have two nested public
static
subclasses Xxx2D.Double
and Xxx2D.Float
to support double
- and float
-precision. High-quality 2D rendering (e.g., rotation, shearing, curve segments) cannot be performed in int
(even though eventually it will be converted to integral screen-pixel values for display). Hence, double
and float
subclasses are introduced in Java 2D for better accuracy and smoothness. The earlier int
-precision classes, such as Point
and Rectangle
are retrofitted as a subclass of Point2D
and Rectangle2D
.
The Xxx2D
, Xxx2D.Double
and Xxx2D.Float
are kept in package java.awt.geom
package.
GeneralPath
The java.awt.geom.GeneralPath
class represents a geometric path constructed from straight lines, quadratic and cubic curves. For example,
int[] x = { -20, 0, 20, 0}; int[] y = { 20, 10, 20, -20}; GeneralPath p = new GeneralPath(); p.moveTo(x[0], y[0]); for (int i = 1; i < x.length; ++i) { p.lineTo(x[i], y[i]); } p.closePath(); g2d.translate(350, 250); g2d.draw(p);
Point2D and its Subclasses Point, Point2D.Double and Point2D.Float (Advanced)
Reference: Source codes of java.awt.geom.Point2D
and java.awt.Point
.
As an example, let's examine Point2D
abstract
superclass and its subclasses more closely.
java.awt.geom.Point2D
Point2D
is an abstract class that cannot be instantiated. It declares the following abstract
methods:
abstract public double getX(); abstract public double getY(); abstract public void setLocation(double x, double y);
It also defines and implemented some static
methods and member methods. (Hence, it is designed as an abstract class, instead of interface.)
// static methods public static double distance(double x1, double y1, double x2, double y2) public static double distanceSq(double x1, double y1, double x2, double y2) // instance (non-static) methods public double distance(double x, double y) public double distanceSq(double x, double y) public double distance(Point2D p) public double distanceSq(Point2D p) ......
Point2D
does not define any instance variable, in particular, the x
and y
location of the point. This is because it is not sure about the type of x
and y
(which could be int
, float
or double
). The instance variables, therefore, are left to the implementation subclasses. Three subclasses were implemented for types of int
, float
and double
, respectively.
Subclasses java.awt.Point, java.awt.geom.Point2D.Double and java.awt.geom.Point2D.Float
How to design these subclasses?
java.awt.Point
is the subclass for int
-precision. It declares instance variables x
and y
as int
, provides implementation to the abstract methods declared in the superclass, and provides some additional methods.
package java.awt; public class Point extends Point2D { // Instance variables (x, y) of type int public int x; public int y; // Constructor public Point(int x, int y) { this.x = x; this.y = y; } // Provide implementation to the abstract methods declared in the superclass public double getX() { return x; } public double getY() { return y; } public void setLocation(double x, double y) { this.x = (int)Math.floor(x + 0.5); this.y = (int)Math.floor(y + 0.5); } // Other methods ...... }
Point
(of int
-precision) is a straight-forward implementation of inheritance and polymorphism. Point
is a legacy class (since JDK 1.1) and retrofitted when Java 2D was introduced. For higher-quality and smoother graphics (e.g., rotation), int
-precision is insufficient. Java 2D, hence, introduced float
-precision and double
-precision points.
Point2D.Double
(for double
-precision point) and Point2D.Float
(for float
-precision point) are, however, implemented as nested public static
subclasses inside the Point2D
outer class.
Recall that nested class (or inner class) can be used for:
- Simplifying access control: inner class can access the
private
variables of the enclosing outer class, as they are at the same level. - Keeping codes together and namespace management.
In this case, it is used for namespace management, as there is no access-control involved.
package java.awt.geom; abstract public class Point2D { // outer class public static class Double extend Point2D { // public static nested class and subclass public double x; public double y; // Constructor public Double(double x, double y) { this.x = x; this.y = y; } // Provides implementation to the abstract methods public double getX() { return x; } public double getY() { return y; } public void setLocation(double x, double y) { this.x = x; this.y = y; } // Other methods ...... } public static class Float extend Point2D { // public static nested class and subclass public float x; public float y; // Constructor public Float(float x, float y) { this.x = x; this.y = y; } // Provide implementation to the abstract methods public double getX() { return x; } public double getY() { return y; } public void setLocation(double x, double y) { this.x = (float)x; this.y = (float)y; } // Other methods ...... } // Definition for the outer class abstract public double getX(); abstract public double getY(); abstract public void setLocation(double x, double y); ...... }
Double
and Float
are static
. In other words, they can be referenced via the classname as Point2D.Double
and Point2D.Float
, and used directly without instantiating the outer class, just like any static
variable or method (e.g., Math.PI
, Math.sqrt()
, Integer.parseInt()
). They are subclasses of Point2D
, and thus can be upcasted to Point2D
.
Point2D.Double p1 = new Point2D.Double(1.1, 2.2);
Point2D.Float p2 = new Point2D.Float(3.3f, 4.4f);
Point p3 = new Point(5, 6);
// You can upcast subclasses Point2D.Double, Point2D.Float and Point to superclass Point2D.
Point2D p4 = new Point2D.Double(1.1, 2.2);
Point2D p5 = new Point2D.Float(3.3f, 4.4f);
Point2D p6 = new Point(5, 6);
In summary, you can treat Point2D.Double
and Point2D.Float
as ordinary classes with a slightly longer name. They were designed as nested class for namespace management, instead of calling them Point2DDouble
and Point2DFloat
.
Note: These classes were designed before JDK 1.5, which introduces generic to support passing of type.
Interface java.awt.Shape
Almost all the Xxx2D
classes (except Point2D
) implements java.awt.Shape
interface. It can be used as argument in Graphics2D
's draw(Shape)
and fill(Shape)
methods.
The Shape
interface declares abstract
methods contains()
and intersects()
, which are useful in game programming for collision detection:
// Is this point within this Shape's bounds? boolean contains(double x, double y) boolean contains(Point2D p) // Is this rectangle within this Shape's bounds? boolean contains(double x, double y, double width, double height) boolean contains(Rectangle2D rect) // Does the interior of the given bounding rectangle intersect with this shape? boolean intersects(double x, double y, double width, double height) boolean intersects(Rectangle2D rect)
The Shape
interface also declares methods to return the bounding rectangle of this shape (again, useful in collision detection).
Rectangle getBounds() // int-precision Rectangle2D getBounds2D() // higher precision version
The Shape
interface declares a method called getPathIterator()
to retrieve a PathIterator
object that can be used to iterate along the Shape
boundary.
PathIterator getPathIterator(AffineTransform at) PathIterator getPathIterator(AffineTransform at, double flatness)
The popular legacy java.awt.Polygon
class was retrofitted to implement Shape
interface. However, there is no Polygon2D
in Java 2D, which can be served by a more generic Path2D
.
[TODO] Example
Stroke, Paint and Composite Attributes
Pen's Stroke
The Graphics2D
's stroke attribute control the pen-stroke used for the outline of a shape. It is set via the Graphics2D
's setStroke()
. A Stroke
object implements java.awt.Stroke
interface. Java 2D provides a built-in java.awt.BasicStroke
.
public BasicStroke(float width, int cap, int join, float miterlimit, float[] dash, float dash_phase)
// All parameters are optional
// width: width of the pen stroke
// cap: the decoration of the ends, CAP_ROUND, CAP_SQUARE or CAP_BUTT.
// join: the decoration where two segments meet, JOIN_ROUND, JOIN_MITER, or JOIN_BEVEL
// miterlimit: the limit to trim the miter join.
// dash: the array representing the dashing pattern.
// dash_phase: the offset to start the dashing pattern
For example,
g2d.setStroke(new BasicStroke(10, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); g2d.setColor(Color.CYAN); g2d.draw(new Rectangle2D.Double(300, 50, 200, 100)); // Test dash-stroke float[] dashPattern = {20, 5, 10, 5}; // dash, space, dash, space g2d.setStroke(new BasicStroke(5, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 10, dashPattern, 0)); g2d.setColor(Color.CYAN); g2d.draw(new Rectangle2D.Double(50, 200, 200, 100));
Paint
The Graphics2D
's paint attribute determines the color used to render the shape. It is set via the Graphics2D
's setPaint()
method. A Paint
object implements the java.awt.Paint
interface. Java 2D provides many built-in Paint
objects such as GradientPaint
, LinearGradientPaint
, RadialGradientPaint
, MultipleGradientPaint
, TexturePaint
, and others.
For example,
g2d.setPaint(new GradientPaint(50, 80, Color.RED, 250, 180, Color.GREEN)); // set current paint context to a GradientPaint, from (x1, y1) with color1 to (x2, y2) with color2 g2d.fill(new Rectangle2D.Double(50, 80, 200, 100)); // fill the Shape with the current paint context
Composite
[TODO] How to compose the drawing of primitive with the underlying graphics area.
Working with Bitmap Images
Reference: Java Tutorial's "2D Graphics" Section "Working with Images" @ http://docs.oracle.com/javase/tutorial/2d/images/index.html"
A bitmap image is a 2D rectangular array of pixels. Each pixel has a color value (typically in RGB or RGBA). The dimension of the image is represented by its width and length in pixels. In Java, the origin (0, 0) of an image is positioned at the top-left corner, like all other components.
Most of the image display and processing methods work on java.awt.Image
. Image
is an abstract
class that represent an image as a rectangular array of pixels. The most commonly-used implementation subclass is java.awt.image.BufferedImage
, which stores the pixels in memory buffer so that they can be directly accessed. A BufferedImage
comprises a ColorModel
and a Raster
of pixels. The ColorModel
provides a color interpretation of the image's pixel data.
An Image
object (and subclass BufferedImage
) can be rendered onto a JComponent
(such as JPanel
) via Graphics
' (or Graphics2D
') drawImage()
method.
Image is typically read in from an external image file into a BufferedImage
(although you can create a BufferedImage
based on algorithm). Image file formats supported by JDK include:
- GIF
- PNG (Portable Network Graphics)
- JPEG
- BMP
BufferedImage
supports image filtering operations (such as convolution). The resultant image can be painted on the screen, sent to printer, or save to an external file.
Transparent vs. Opaque Background
PNG and GIF supports transparent background. JPEG does not. PNG and GIF are palette-based. They maintain a list of palettes and map each palette number to a RGB color value. Every image pixel is then labeled with a palette number, which is then mapped to the actual RGB values. One of the palette number can be designated as transparent. Pixels with this transparent palette value will not be displayed. Instead, the background of the image is displayed.
Loading Images
There are several ways to create an Image
for use in your program.
Using ImageIO.read() (JDK 1.4)
The easiest way to load an image into your program is to use the static
method ImageIO.read()
of the javax.imageio.ImageIO
class, which returns a java.awt.image.BufferedImage
. Similar, you can use ImageIO.write()
to write an image.
public static BufferedImage read(URL imagePath) throws IOException public static BufferedImage read(File imagePath) throws IOException public static BufferedImage read(InputStream stream) throws IOException public static BufferedImage read(ImageInputStream stream) throws IOException
Instead of using java.io.File
class to handle a disk file, it is better to use java.net.URL
. URL
is more flexible and can handle files from more sources, such as disk file and JAR file (used for distributing your program). It works on application as well as applet. For example,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
import java.awt.*; import java.io.IOException; import java.net.URL; import javax.imageio.ImageIO; import javax.swing.*; /** Test loading an external image into a BufferedImage using ImageIO.read() */ @SuppressWarnings("serial") public class LoadImageDemo extends JPanel { // Named-constants public static final int CANVAS_WIDTH = 640; public static final int CANVAS_HEIGHT = 480; public static final String TITLE = "Load Image Demo"; private String imgFileName = "images/duke.gif"; // relative to project root (or bin) private Image img; // a BufferedImage object /** Constructor to set up the GUI components */ public LoadImageDemo() { // Load an external image via URL URL imgUrl = getClass().getClassLoader().getResource(imgFileName); if (imgUrl == null) { System.err.println("Couldn't find file: " + imgFileName); } else { try { img = ImageIO.read(imgUrl); } catch (IOException ex) { ex.printStackTrace(); } } setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT)); } /** Custom painting codes on this JPanel */ @Override public void paintComponent(Graphics g) { super.paintComponent(g); // paint background setBackground(Color.WHITE); g.drawImage(img, 50, 50, null); } /** The Entry main method */ public static void main(String[] args) { // Run the GUI codes on the Event-Dispatching thread for thread safety SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JFrame frame = new JFrame("Load Image Demo"); frame.setContentPane(new LoadImageDemo()); frame.pack(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } }); } } |
Using Toolkit's getImage()
// In java.awt.Toolkit
public abstract Image getImage(URL url)
public abstract Image getImage(String filename)
For example,
import java.awt.Toolkit; ...... Toolkit tk = Toolkit.getDefaultToolkit(); Image img = tk.getImage("images/duke.gif");
Via ImageIcon's getImage()
ImageIcon
is used to decorate JComponent
s (such as JLabel
and JButton
). Construct an ImageIcon
and get an Image
via ImageIcon
's getImage()
. For example,
ImageIcon icon = null; String imgFilename = "images/duke.gif"; java.net.URL imgURL = getClass().getClassLoader().getResource(imgFilename); if (imgURL != null) { icon = new ImageIcon(imgURL); } else { System.err.println("Couldn't find file: " + imgFilename); } Image img = icon.getImage();
On the other hand, you can also construct an ImageIcon
from an Image
object via constructor:
public ImageIcon(Image image) // Construct an ImageIcon from the Image object
[TODO] Benchmark these methods for small and large images.
drawImage()
The java.awt.Graphics
class declares 6 versions of drawImage()
for drawing bitmap images. The subclass java.awt.Graphics2D
adds a few more.
// In class java.awt.Graphics public abstract boolean drawImage(Image img, int x, int y, ImageObserver observer) public abstract boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) public abstract boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) public abstract boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer) // The img is drawn with its top-left corner at (x, y) scaled to the specified width and height // (default to the image's width and height). // The bgColor (background color) is used for "transparent" pixels. public abstract boolean drawImage(Image img, int destX1, int destY1, int destX2, int destY2, int srcX1, int srcY1, int srcX2, int srcY2, ImageObserver observer) public abstract boolean drawImage(Image img, int destX1, int destY1, int destX2, int destY2, int srcX1, int srcY1, int srcX2, int srcY2, Color bgcolor, ImageObserver observer) // The img "clip" bounded by (scrX1, scrY2) and (scrX2, srcY2) is scaled and drawn from // (destX1, destY1) to (destX2, destY2).
The coordinates involved is shown in the above diagram. The ImageObserver
receives notification about the Image
as it is loaded. In most purposes, you can set it to null
, or the custom drawing JPanel
(via this
). The ImageObserver
is not needed for BufferedImage
, and shall be set to null
.
Graphics2D's drawImage()
Graphics2D
supports affine transform and image filtering operations on images, as follows:
// In class java.awt.Graphics2D public abstract boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) // Apply the specified AffineTransform to the image public abstract void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) // Apply the specified image filtering operation to the image public abstract void drawRenderedImage(RenderedImage img, AffineTransform xform) public abstract void drawRenderableImage(RenderableImage img, AffineTransform xform)
Image Affine Transforms
Java 2D's affine transform works on bitmap image as well as vector graphics. However, instead of manipulating the Graphics2D
's current transform context (which operates on vector-graphics only via rendering methods drawXxx()
and fillXxx()
), you need to allocate an AffineTransform
object to perform transformation on images.
Code Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
import java.awt.geom.AffineTransform; import javax.imageio.ImageIO; import java.net.URL; import java.awt.*; import javax.swing.*; import java.io.*; /** Test applying affine transform on images */ @SuppressWarnings("serial") public class ImageTransformDemo extends JPanel { // Named-constants for dimensions public static final int CANVAS_WIDTH = 640; public static final int CANVAS_HEIGHT = 480; public static final String TITLE = "Image Transform Demo"; // Image private String imgFileName = "images/duke.png"; // relative to project root or bin private Image img; private int imgWidth, imgHeight; // width and height of the image private double x = 100.0, y = 80.0; // center (x, y), with initial value /** Constructor to set up the GUI components */ public ImageTransformDemo() { // URL can read from disk file and JAR file URL url = getClass().getClassLoader().getResource(imgFileName); if (url == null) { System.err.println("Couldn't find file: " + imgFileName); } else { try { img = ImageIO.read(url); imgWidth = img.getWidth(this); imgHeight = img.getHeight(this); } catch (IOException ex) { ex.printStackTrace(); } } this.setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT)); } /** Custom painting codes on this JPanel */ @Override public void paintComponent(Graphics g) { super.paintComponent(g); // paint background setBackground(Color.WHITE); Graphics2D g2d = (Graphics2D) g; g2d.drawImage(img, 0, 0, this); // Display with top-left corner at (0, 0) // drawImage() does not use the current transform of the Graphics2D context // Need to create a AffineTransform and pass into drawImage() AffineTransform transform = new AffineTransform(); // identity transform // Display the image with its center at the initial (x, y) transform.translate(x - imgWidth/2, y - imgHeight/2); g2d.drawImage(img, transform, this); // Try applying more transform to this image for (int i = 0; i < 5; ++i) { transform.translate(70.0, 5.0); transform.rotate(Math.toRadians(15), imgWidth/2, imgHeight/2); // about its center transform.scale(0.9, 0.9); g2d.drawImage(img, transform, this); } } /** The Entry main method */ public static void main(String[] args) { // Run the GUI codes on the Event-Dispatching thread for thread safety SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JFrame frame = new JFrame(TITLE); frame.setContentPane(new ImageTransformDemo()); frame.pack(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); // center the application window frame.setVisible(true); } }); } } |
Image Filtering Operations
Graphics2D
supports image filtering operations via the following drawImage()
method:
public abstract void drawImage(BufferedImage img, BufferedImageOp op, int x, int y)
// Apply the specified image filtering operation to the image
Many built-in image filtering operations are available in java.awt.image
package.
[TODO] more and example
Animating Image Frames
There are two ways to organize animated image frames:
- Keep each of the frames in its own file.
- Keep all of the frames in a stripe (1D or 2D) in a single file for better organization and faster loading.
Code Example 1: Each Frame in its Own File
Three image frames (in its own file) was used in this example, as follow:
In a typical game, the actor has a (x, y)
position, move at a certain speed
(in pixels per move-step) and direction
(in degrees), and may rotate at a rotationSpeed
(in degrees per move-step).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
import java.awt.geom.AffineTransform; import javax.imageio.ImageIO; import java.net.URL; import java.awt.*; import javax.swing.*; import java.io.*; /** Animating image frames. Each frame has its own file */ @SuppressWarnings("serial") public class AnimatedFramesDemo extends JPanel { // Named-constants static final int CANVAS_WIDTH = 640; static final int CANVAS_HEIGHT = 480; public static final String TITLE = "Animated Frame Demo"; private String[] imgFilenames = { "images/pacman_1.png", "images/pacman_2.png", "images/pacman_3.png"}; private Image[] imgFrames; // array of Images to be animated private int currentFrame = 0; // current frame number private int frameRate = 5; // frame rate in frames per second private int imgWidth, imgHeight; // width and height of the image private double x = 100.0, y = 80.0; // (x,y) of the center of image private double speed = 8; // displacement in pixels per move private double direction = 0; // in degrees private double rotationSpeed = 1; // in degrees per move // Used to carry out the affine transform on images private AffineTransform transform = new AffineTransform(); /** Constructor to set up the GUI components */ public AnimatedFramesDemo() { // Setup animation loadImages(imgFilenames); Thread animationThread = new Thread () { @Override public void run() { while (true) { update(); // update the position and image repaint(); // Refresh the display try { Thread.sleep(1000 / frameRate); // delay and yield to other threads } catch (InterruptedException ex) { } } } }; animationThread.start(); // start the thread to run animation // Setup GUI setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT)); } /** Helper method to load all image frames, with the same height and width */ private void loadImages(String[] imgFileNames) { int numFrames = imgFileNames.length; imgFrames = new Image[numFrames]; // allocate the array URL imgUrl = null; for (int i = 0; i < numFrames; ++i) { imgUrl = getClass().getClassLoader().getResource(imgFileNames[i]); if (imgUrl == null) { System.err.println("Couldn't find file: " + imgFileNames[i]); } else { try { imgFrames[i] = ImageIO.read(imgUrl); // load image via URL } catch (IOException ex) { ex.printStackTrace(); } } } imgWidth = imgFrames[0].getWidth(null); imgHeight = imgFrames[0].getHeight(null); } /** Update the position based on speed and direction of the sprite */ public void update() { x += speed * Math.cos(Math.toRadians(direction)); // x-position if (x >= CANVAS_WIDTH) { x -= CANVAS_WIDTH; } else if (x < 0) { x += CANVAS_WIDTH; } y += speed * Math.sin(Math.toRadians(direction)); // y-position if (y >= CANVAS_HEIGHT) { y -= CANVAS_HEIGHT; } else if (y < 0) { y += CANVAS_HEIGHT; } direction += rotationSpeed; // update direction based on rotational speed if (direction >= 360) { direction -= 360; } else if (direction < 0) { direction += 360; } ++currentFrame; // display next frame if (currentFrame >= imgFrames.length) { currentFrame = 0; } } /** Custom painting codes on this JPanel */ @Override public void paintComponent(Graphics g) { super.paintComponent(g); // paint background setBackground(Color.WHITE); Graphics2D g2d = (Graphics2D) g; transform.setToIdentity(); // The origin is initially set at the top-left corner of the image. // Move the center of the image to (x, y). transform.translate(x - imgWidth / 2, y - imgHeight / 2); // Rotate about the center of the image transform.rotate(Math.toRadians(direction), imgWidth / 2, imgHeight / 2); // Apply the transform to the image and draw g2d.drawImage(imgFrames[currentFrame], transform, null); } /** The Entry main method */ public static void main(String[] args) { // Run the GUI codes on the Event-Dispatching thread for thread safety SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JFrame frame = new JFrame(TITLE); frame.setContentPane(new AnimatedFramesDemo()); frame.pack(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); // center the application window frame.setVisible(true); } }); } } |
Dissecting the Program
[TODO]
Code Example 2: Frames Organized in a Stripe
In this example, all the frames of an animated sequence are kept in a single file organized in rows and columns.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
import javax.imageio.ImageIO; import java.net.URL; import java.awt.*; import javax.swing.*; import java.io.*; /** Animating image frames. All frames kept in a stripe. */ @SuppressWarnings("serial") public class AnimatedFramesInStripe extends JPanel { // Named-constants static final int CANVAS_WIDTH = 640; static final int CANVAS_HEIGHT = 480; public static final String TITLE = "Animated Frame Demo"; private String imgFilename = "images/GhostStripe.png"; private int numRows, numCols, numFrames; private Image img; // for the entire image stripe private int currentFrame; // current frame number private int frameRate = 5; // frame rate in frames per second private int imgWidth, imgHeight; // width and height of the image private double x = 100.0, y = 80.0; // (x,y) of the center of image private double speed = 8; // displacement in pixels per move private double direction = 0; // in degrees private double rotationSpeed = 1; // in degrees per move /** Constructor to set up the GUI components */ public AnimatedFramesInStripe() { // Setup animation loadImage(imgFilename, 2, 4); Thread animationThread = new Thread () { @Override public void run() { while (true) { update(); // update the position and image repaint(); // Refresh the display try { Thread.sleep(1000 / frameRate); // delay and yield to other threads } catch (InterruptedException ex) { } } } }; animationThread.start(); // start the thread to run animation // Setup GUI setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT)); } /** Helper method to load image. All frames have the same height and width */ private void loadImage(String imgFileName, int numRows, int numCols) { URL imgUrl = getClass().getClassLoader().getResource(imgFileName); if (imgUrl == null) { System.err.println("Couldn't find file: " + imgFileName); } else { try { img = ImageIO.read(imgUrl); // load image via URL } catch (IOException ex) { ex.printStackTrace(); } } numFrames = numRows * numCols; this.imgHeight = img.getHeight(null) / numRows; this.imgWidth = img.getWidth(null) / numCols; this.numRows = numRows; this.numCols = numCols; currentFrame = 0; } /** Returns the top-left x-coordinate of the given frame number. */ private int getCurrentFrameX() { return (currentFrame % numCols) * imgWidth; } /** Returns the top-left y-coordinate of the given frame number. */ private int getCurrentFrameY() { return (currentFrame / numCols) * imgHeight; } /** Update the position based on speed and direction of the sprite */ public void update() { x += speed * Math.cos(Math.toRadians(direction)); // x-position if (x >= CANVAS_WIDTH) { x -= CANVAS_WIDTH; } else if (x < 0) { x += CANVAS_WIDTH; } y += speed * Math.sin(Math.toRadians(direction)); // y-position if (y >= CANVAS_HEIGHT) { y -= CANVAS_HEIGHT; } else if (y < 0) { y += CANVAS_HEIGHT; } direction += rotationSpeed; // update direction based on rotational speed if (direction >= 360) { direction -= 360; } else if (direction < 0) { direction += 360; } ++currentFrame; // displays next frame if (currentFrame >= numFrames) { currentFrame = 0; } } /** Custom painting codes on this JPanel */ @Override public void paintComponent(Graphics g) { super.paintComponent(g); // paint background setBackground(Color.WHITE); Graphics2D g2d = (Graphics2D) g; int frameX = getCurrentFrameX(); int frameY = getCurrentFrameY(); g2d.drawImage(img, (int)x - imgWidth / 2, (int)y - imgHeight / 2, (int)x + imgWidth / 2, (int)y + imgHeight / 2, frameX, frameY, frameX + imgWidth, frameY + imgHeight, null); } /** The Entry main method */ public static void main(String[] args) { // Run the GUI codes on the Event-Dispatching thread for thread safety SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JFrame frame = new JFrame(TITLE); frame.setContentPane(new AnimatedFramesInStripe()); frame.pack(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); // center the application window frame.setVisible(true); } }); } } |
Dissecting the Program
[TODO]
High Performance Graphics
Full-Screen Display Mode (JDK 1.4)
Reference: Java Tutorial's "Full-Screen Exclusive Mode API" @http://docs.oracle.com/javase/tutorial/extra/fullscreen/index.html.
You could check if full-screen mode is supported in your graphics environment by invoking isFullScreenSupported()
of the screen GraphicsDevice
:
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice defaultScreen = env.getDefaultScreenDevice(); System.out.println("isFullScreenSupported: " + defaultScreen.isFullScreenSupported()); // Enter fullscreen mode setUndecorated(true); setResizable(false); defaultScreen.setFullScreenWindow(this); // "this" JFrame
To enter fullscreen mode, use GraphicsDevice
's setFullScreenWindow(JFrame)
. To leave the fullscreen mode and return to windowed mode, use setFullScreenWindow(null)
. You should not try to setSize()
or resize the window in full screen mode.
You could run your program in fullscreen (without the window's title bar) by invoking JFrame
's setUndecorated(true)
.
There are a few ways to find out the current screen size:
// via the default Toolkit
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
int screenWidth = (int)dim.getWidth();
int screenHeight = (int)dim.getHeight();
Code Example 1: Running in Fullscreen Mode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
import java.awt.*; import java.awt.event.*; import javax.swing.*; /** Testing the full-screen mode */ @SuppressWarnings("serial") public class FullScreenDemo extends JFrame { /** Constructor to set up the GUI components */ public FullScreenDemo() { // Check if full screen mode supported? GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice defaultScreen = env.getDefaultScreenDevice(); if (!defaultScreen.isFullScreenSupported()) { System.err.println("Full Screen mode is not supported!"); System.exit(1); } // Use ESC key to quit addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { System.exit(0); } } }); setContentPane(new DrawCanvas()); setDefaultCloseOperation(EXIT_ON_CLOSE); setUndecorated(true); setResizable(false); defaultScreen.setFullScreenWindow(this); // full-screen mode setVisible(true); } /** DrawCanvas (inner class) is a JPanel used for custom drawing */ private class DrawCanvas extends JPanel { @Override public void paintComponent(Graphics g) { super.paintComponent(g); setBackground(Color.BLACK); // Paint messages g.setColor(Color.YELLOW); g.setFont(new Font(Font.DIALOG, Font.BOLD, 30)); FontMetrics fm = g.getFontMetrics(); String msg = "In Full-Screen mode"; int msgWidth = fm.stringWidth(msg); int msgAscent = fm.getAscent(); int msgX = getWidth() / 2 - msgWidth / 2; int msgY = getHeight() / 2 + msgAscent / 2; g.drawString(msg, msgX, msgY); g.setColor(Color.WHITE); g.setFont(new Font(Font.DIALOG, Font.PLAIN, 18)); fm = g.getFontMetrics(); msg = "Press ESC to quit"; msgWidth = fm.stringWidth(msg); int msgHeight = fm.getHeight(); msgX = getWidth() / 2 - msgWidth / 2; msgY += msgHeight * 1.5; g.drawString(msg, msgX, msgY); } } /** The Entry main method */ public static void main(String[] args) { // Run the GUI codes on the Event-Dispatching thread for thread safety SwingUtilities.invokeLater(new Runnable() { @Override public void run() { new FullScreenDemo(); } }); } } |
Code Example 2: Switching between Fullscreen and Windowed Mode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
import java.awt.*; import java.awt.event.*; import javax.swing.*; /** Testing the full-screen mode */ @SuppressWarnings("serial") public class FullScreenEscDemo extends JFrame { // Windowed mode settings private static String winModeTitle = "Switching between Full Screen Mode and Windowed Mode Demo"; private static int winModeX, winModeY; // top-left corner (x, y) private static int winModeWidth, winModeHeight; // width and height private boolean inFullScreenMode; // in fullscreen or windowed mode? private boolean fullScreenSupported; // is fullscreen supported? /** Constructor to set up the GUI components */ public FullScreenEscDemo() { // Get the screen width and height Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); // Set the windowed mode initial width and height to about fullscreen winModeWidth = (int)dim.getWidth(); winModeHeight = (int)dim.getHeight() - 35; // minus task bar winModeX = 0; winModeY = 0; // Check if full screen mode supported? GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); final GraphicsDevice defaultScreen = env.getDefaultScreenDevice(); fullScreenSupported = defaultScreen.isFullScreenSupported(); if (fullScreenSupported) { setUndecorated(true); setResizable(false); defaultScreen.setFullScreenWindow(this); // full-screen mode inFullScreenMode = true; } else { setUndecorated(false); setResizable(true); defaultScreen.setFullScreenWindow(null); // windowed mode setBounds(winModeX, winModeY, winModeWidth, winModeHeight); inFullScreenMode = false; } // Use ESC key to switch between Windowed and fullscreen modes this.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_SPACE) { if (fullScreenSupported) { if (!inFullScreenMode) { // Switch to fullscreen mode setVisible(false); setResizable(false); dispose(); setUndecorated(true); defaultScreen.setFullScreenWindow(FullScreenEscDemo.this); setVisible(true); } else { // Switch to windowed mode setVisible(false); dispose(); setUndecorated(false); setResizable(true); defaultScreen.setFullScreenWindow(null); setBounds(winModeX, winModeY, winModeWidth, winModeHeight); setVisible(true); } inFullScreenMode = !inFullScreenMode; repaint(); } } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { System.exit(0); } } }); // To save the window width and height if the window has been resized. this.addComponentListener(new ComponentAdapter() { @Override public void componentMoved(ComponentEvent e) { if (!inFullScreenMode) { winModeX = getX(); winModeY = getY(); } } @Override public void componentResized(ComponentEvent e) { if (!inFullScreenMode) { winModeWidth = getWidth(); winModeHeight = getHeight(); } } }); setContentPane(new DrawCanvas()); setDefaultCloseOperation(EXIT_ON_CLOSE); setTitle(winModeTitle); setVisible(true); } /** DrawCanvas (inner class) is a JPanel used for custom drawing */ private class DrawCanvas extends JPanel { @Override public void paintComponent(Graphics g) { super.paintComponent(g); setBackground(Color.BLACK); // Draw a box to indicate the borders Graphics2D g2d = (Graphics2D)g; g2d.setStroke(new BasicStroke(8)); g2d.setColor(Color.RED); g2d.drawRect(0, 0, getWidth()-1, getHeight()-1); // Paint messages g.setColor(Color.YELLOW); g.setFont(new Font(Font.DIALOG, Font.BOLD, 30)); FontMetrics fm = g.getFontMetrics(); String msg = inFullScreenMode ? "In Full-Screen mode" : "In Windowed mode"; int msgWidth = fm.stringWidth(msg); int msgAscent = fm.getAscent(); int msgX = getWidth() / 2 - msgWidth / 2; int msgY = getHeight() / 2 + msgAscent / 2; g.drawString(msg, msgX, msgY); g.setColor(Color.WHITE); g.setFont(new Font(Font.DIALOG, Font.PLAIN, 18)); fm = g.getFontMetrics(); msg = "Press SPACE to toggle between Full-screen/windowed modes, ESC to quit."; msgWidth = fm.stringWidth(msg); int msgHeight = fm.getHeight(); msgX = getWidth() / 2 - msgWidth / 2; msgY += msgHeight * 1.5; g.drawString(msg, msgX, msgY); } } /** The Entry main method */ public static void main(String[] args) { // Run the GUI codes on the Event-Dispatching thread for thread safety SwingUtilities.invokeLater(new Runnable() { @Override public void run() { new FullScreenEscDemo(); } }); } } |
Rendering to the Display & Double Buffering
The common problems in rendering a graphic object or image to the display are:
- Flashing (or flickering): caused by clearing the display and then drawing the graphics.
- Image Tearing: For a moving object, the user sees part of the new image and part of the old one.
The common way to resolve these display rendering problem is via so-called double buffering.
A few techniques are available in Java for double buffering:
- BufferStrategy (JDK 1.4)
- BufferedImage
- other??
[TODO]
Splash Screen
To show a splash screen before launching your application, include command-line VM argument "-splash:splashImageFilename
" to display the image.
To show a progress bar over the splash screen, need to write some codes to overlay the progress bar on top of the splash screen. The following shows a simulation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import java.awt.*; /** Splash Screen Demo (with a Progress Bar) Run with VM command-line option -splash:splashImageFilename */ public class SplashScreenDemo { public static void main(String[] args) { SplashScreen splash = SplashScreen.getSplashScreen(); if (splash == null) { System.err.println("Splash Screen not available!"); } else { // Okay, Splash screen created Dimension splashBounds = splash.getSize(); Graphics2D g2d = splash.createGraphics(); // Simulate a progress bar for (int i = 0; i < 100; i += 5) { g2d.setColor(Color.RED); g2d.fillRect(0, splashBounds.height / 2, splashBounds.width * i / 100, 20); splash.update(); try { Thread.sleep(200); // Some delays } catch (Exception e) {} } g2d.dispose(); splash.close(); } } } |
[TODO] Can we use the JProgressBar
class?
REFERENCES & RESOURCES
- Java 2D Tutorial @ http://docs.oracle.com/javase/tutorial/2d/TOC.html.
- Java Tutorial's "Full-Screen Exclusive Mode API" @ http://docs.oracle.com/javase/tutorial/extra/fullscreen/index.html.
- Jonathan S. Harbour, "Beginning Java 5 Game Programming".