JavaMontgomeryAppletSeries
From Wiki
This is the support page for John Montgomery's Writing a Java Applet video set. Please feel free to dip-in and extend this information.
Contents |
The Applet Lifecycle
See: http://java.sun.com/docs/books/tutorial/deployment/applet/lifeCycle.html
- init() will be called once when the applet is first loaded
- start() will be called multiple times (theoretically whenever the user returns the page)
- stop() will be called mulitple time (theoretically whenever the user leaves the page)
- destroy() will be called when the applet is about to be removed from memory
Other methods:
- paint(Graphics g) is used for rendering to the screen (this will sometimes be called directly by the window manager)
- update(Graphics g) is usually the method first called to update the applet's appearance on screen (e.g. from a repaint() call) however it is not always used. (by default it clears the applet and then calls paint)
Example Code
Code for hello world applet
In TestApplet.java:
import java.applet.*;
import java.awt.*;
public class TestApplet extends Applet {
public void paint( Graphics g ) {
g.drawString( "Hello World", getWidth()/2, getHeight()/2 );
}
}
html snippet needed to run the applet (e.g. place in index.html):
<applet code=TestApplet width=256 height=256>
<!- stuff in here should be seen if the user does not have java -->
<applet>
To compile and run at the command line:
>javac TestApplet.java >appletviewer index.html
Code for mouse handling
To receive mouse events we need to call the Applets addMouseListener method and pass it a MouseListener.
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
public class TestApplet extends Applet {
private Point position = null;
public void init() {
// MouseAdapter implements MouseListener, but
// provides stubs of the methods, so it is easier to only
// override one method later
addMouseListener(
new MouseAdapter() {
public void mouseClicked(MouseEvent me) {
position = new Point( me.getX(), me.getY() );
repaint();
}
}
)
}
public void paint( Graphics g ) {
int x = getWidth()/2;
int y = getHeight()/2;
if ( position != null ) {
x = position.x;
y = position.y;
}
g.drawString( "Hello World", x, y );
}
}
Code for double-buffering
On a side note if a JApplet is used rather than an Applet then we do not need to manually handle double-buffering.
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
public class TestApplet extends Applet {
private Image buffer = null;
private Graphics bg = null;
private Point position = null;
public void init() {
buffer = createImage( getWidth(), getHeight() );
bg = buffer.getGraphics();
// MouseAdapter implements MouseListener, but
// provides stubs of the methods, so it is easier to only
// override one method later
addMouseListener(
new MouseAdapter() {
public void mouseClicked(MouseEvent me) {
position = new Point( me.getX(), me.getY() );
repaint();
}
}
)
}
public void update( Graphics g ) {
paint(g);
}
public void paint( Graphics g ) {
int x = getWidth()/2;
int y = getHeight()/2;
if ( position != null ) {
x = position.x;
y = position.y;
}
// draw into the buffer
bg.setColor( Color.WHITE );
bg.fillRect( 0, 0, getWidth(), getHeight() );
bg.setColor( Color.RED );
bg.drawString( "Hello World", x, y );
// draw the buffer onto the screen
g.drawImage( buffer, 0, 0, null );
}
}
Converting an applet to run as an application
An applet relies partially on being run in a web browser (e.g. some sort of applet context), but it is often the case that an applet does not specifically need to be run that way. With a bit of care and testing it is possibly to have an applet that can also be run as a standalone application. As a java.applet.Applet is just another awt component code roughly like the following can be used:
// just showing the additions to the test applet (rest is snipped for clarity)
import java.awt.event.*;
public class TestApplet extends Applet {
public Dimension getPreferredSize() {
// overridden so app knows how big we want the applet to be
return new Dimension(256,256);
}
public static void main(String[] args) {
Frame frame = new Frame("My Test Applet");
frame.addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent we) {
// this is called when user tries to close the window
// and is a good spot to double check we want to close the window
((Frame)we.getSource()).dispose(); // this does the actual closing
}
public void windowClosed(WindowEvent we) {
// this is called when the window is actually closed (e.g. after calling dispose())
System.exit(0); // force VM to terminate and app to finish
}
}
);
Applet applet = new TestApplet();
frame.add( applet );
frame.pack();
applet.init(); // have to call this ourselves now
frame.show();
// now call start on the applet
applet.start(); // again have to do this ourselves
}
}
Full source for the videos
This still needs to be filled in - can you help?
Applet 1 (The 'Hello World' Applet)
Applet 2
Applet 3
Applet 4
Applet 5
Applet 6 (Mandelbrot fractal applet in grey)
Applet 7
Applet 8
Applet 9 (Navigating using the mouse)
import java.applet.Applet;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class TestApplet extends Applet implements Runnable {
private Point position = null;
private Point hello = new Point(0,0);
private Object bufferLock = new Object();
private BufferedImage buffer = null;
private int[] pixels = null;
private Rectangle2D.Float initialBounds = new Rectangle2D.Float( -2.1f, -1.5f, 3.0f, 3.0f );
private Rectangle2D.Float bounds = new Rectangle2D.Float( initialBounds.x, initialBounds.y, initialBounds.width, initialBounds.height );
private Thread thread = null;
public final static int MAX_ITERATIONS = 32;
public void start() {
thread = new Thread( this );
thread.start();
}
public void init() {
synchronized( bufferLock ) {
buffer = new BufferedImage( getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB );
pixels = new int[ buffer.getWidth()*buffer.getHeight() ];
clearPixels();
}
addMouseListener(
new MouseAdapter() {
public void mouseClicked(MouseEvent me) {
handleClick( me.getX(), me.getY() );
}
}
);
render();
}
public void run() {
while( thread == Thread.currentThread() ) {
tick();
render();
try {
Thread.sleep(40);
}
catch( InterruptedException ie ) {}
}
}
public void stop() {
thread = null;
}
protected void handleClick( int x, int y ) {
int screenWidth = getWidth();
int screenHeight = getHeight();
float mx = (bounds.x + bounds.width*(x/(float)screenWidth));
float my = (bounds.y + bounds.height*(y/(float)screenHeight));
float cx = bounds.x + bounds.width/2;
float cy = bounds.y + bounds.height/2;
float dx = mx-cx;
float dy = my-cy;
bounds.x += dx;
bounds.y += dy;
}
protected void tick() {
if ( position != null ) {
if ( hello.x < position.x ) hello.x++;
else hello.x--;
if ( hello.y < position.y ) hello.y++;
else hello.y--;
}
}
protected int calcIterations( float x, float y ) {
int iterations = 0;
float xn = x, yn = y;
while ( iterations < MAX_ITERATIONS ) {
float xn1 = xn*xn - yn*yn;
float yn1 = 2*xn*yn;
xn = xn1 + x;
yn = yn1 + y;
float magsq = xn*xn + yn*yn;
if ( magsq > 4 )
break;
iterations++;
}
return iterations;
}
private int rgb( int r, int g, int b ) {
return (r << 16) | (g << 8) | b;
}
private void setPixel( int x, int y, int rgb ) {
if ( x >= 0 && y >= 0 && x < buffer.getWidth() && y < buffer.getHeight() ) {
pixels[ y*buffer.getWidth() + x ] = rgb;
}
}
private void clearPixels() {
for ( int i = 0; i < pixels.length; i++ ) pixels[ i ] = rgb( 0, 0, 0 );
}
private void copyPixels() {
buffer.setRGB( 0, 0, buffer.getWidth(), buffer.getHeight(), pixels, 0, buffer.getWidth() );
}
protected void copyPixelsAndPaint() {
synchronized( bufferLock ) {
copyPixels();
}
repaint();
try {
Thread.sleep(40);
}
catch( InterruptedException ie ) {}
}
protected int calcPixelColor( int iterations ) {
int gray = (255*iterations)/MAX_ITERATIONS;
int r = 80 + 5*(gray*gray)/255;
int g = 5*(gray*gray)/255;
int b = gray;
r = Math.min( r, 255 );
g = Math.min( g, 255 );
b = Math.min( b, 255 );
return rgb( r, g, b );
}
protected void renderMandelbrot( float x, float y, float width, float height ) {
int screenWidth = buffer.getWidth();
int screenHeight = buffer.getHeight();
List pixels = new LinkedList();
for ( int i = 0; i < screenWidth; i++ ) {
for ( int j = 0; j < screenHeight; j++ ) {
pixels.add( new Point( i, j ) );
}
}
Collections.shuffle( pixels );
long start = System.currentTimeMillis();
while( !pixels.isEmpty() ) {
Point pt = (Point)pixels.remove(0);
int i = pt.x;
int j = pt.y;
float xi = (x + width*(i/(float)screenWidth));
float yi = (y + height*(j/(float)screenHeight));
int iterations = calcIterations( xi, yi );
int pixel = calcPixelColor( iterations );
setPixel( i, j, pixel );
if ( (System.currentTimeMillis()-start) > 10 ) {
copyPixelsAndPaint();
start = System.currentTimeMillis();
}
}
}
protected void render() {
renderMandelbrot( bounds.x, bounds.y, bounds.width, bounds.height );
synchronized( bufferLock ) {
copyPixels();
}
repaint();
}
public void update(Graphics g) {
paint( g );
}
public void paint( Graphics g ) {
synchronized( bufferLock ) {
if ( buffer != null )
g.drawImage( buffer, 0, 0, null );
}
}
}
Applet 10 (Zooming using the mouse wheel)
This represents the source code from the final applet in the series. It probably needs tidying up (e.g. to remove code that is now redundant). It is also not terribly efficient, mainly because it is constantly trying to render the fractal - even when nothing needs to be changed on screen.
import java.applet.Applet;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class TestApplet extends Applet implements Runnable {
private Point position = null;
private Point hello = new Point(0,0);
private Object bufferLock = new Object();
private BufferedImage buffer = null;
private int[] pixels = null;
private Rectangle2D.Float initialBounds = new Rectangle2D.Float( -2.1f, -1.5f, 3.0f, 3.0f );
private Rectangle2D.Float bounds = new Rectangle2D.Float( initialBounds.x, initialBounds.y, initialBounds.width, initialBounds.height );
private Thread thread = null;
public final static int MAX_ITERATIONS = 32;
public void start() {
thread = new Thread( this );
thread.start();
}
public void init() {
synchronized( bufferLock ) {
buffer = new BufferedImage( getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB );
pixels = new int[ buffer.getWidth()*buffer.getHeight() ];
clearPixels();
}
addMouseListener(
new MouseAdapter() {
public void mouseClicked(MouseEvent me) {
handleClick( me.getX(), me.getY() );
}
}
);
addMouseWheelListener(
new MouseWheelListener() {
public void mouseWheelMoved(MouseWheelEvent mwe) {
if ( mwe.getWheelRotation() < 0 )
zoomin();
else
zoomout();
}
}
);
}
public void run() {
while( thread == Thread.currentThread() ) {
tick();
render();
try {
Thread.sleep(40);
}
catch( InterruptedException ie ) {}
}
}
public void stop() {
thread = null;
}
protected void handleClick( int x, int y ) {
int screenWidth = getWidth();
int screenHeight = getHeight();
float mx = (bounds.x + bounds.width*(x/(float)screenWidth));
float my = (bounds.y + bounds.height*(y/(float)screenHeight));
float cx = bounds.x + bounds.width/2;
float cy = bounds.y + bounds.height/2;
float dx = mx-cx;
float dy = my-cy;
bounds.x += dx;
bounds.y += dy;
}
protected void zoom( float factor ) {
float newWidth = bounds.width*factor;
float newHeight = bounds.height*factor;
float dw = bounds.width - newWidth;
float dh = bounds.height - newHeight;
float newX = bounds.x + 0.5f*dw;
float newY = bounds.y + 0.5f*dh;
bounds.setRect( newX, newY, newWidth, newHeight );
}
protected void zoomin() {
zoom(0.9f);
}
protected void zoomout() {
zoom(1.1f);
}
protected void tick() {
if ( position != null ) {
if ( hello.x < position.x ) hello.x++;
else hello.x--;
if ( hello.y < position.y ) hello.y++;
else hello.y--;
}
}
protected int calcIterations( float x, float y ) {
int iterations = 0;
float xn = x, yn = y;
while ( iterations < MAX_ITERATIONS ) {
float xn1 = xn*xn - yn*yn;
float yn1 = 2*xn*yn;
xn = xn1 + x;
yn = yn1 + y;
float magsq = xn*xn + yn*yn;
if ( magsq > 4 )
break;
iterations++;
}
return iterations;
}
private int rgb( int r, int g, int b ) {
return (r << 16) | (g << 8) | b;
}
private void setPixel( int x, int y, int rgb ) {
if ( x >= 0 && y >= 0 && x < buffer.getWidth() && y < buffer.getHeight() ) {
pixels[ y*buffer.getWidth() + x ] = rgb;
}
}
private void clearPixels() {
for ( int i = 0; i < pixels.length; i++ ) pixels[ i ] = rgb( 0, 0, 0 );
}
private void copyPixels() {
buffer.setRGB( 0, 0, buffer.getWidth(), buffer.getHeight(), pixels, 0, buffer.getWidth() );
}
protected void copyPixelsAndPaint() {
synchronized( bufferLock ) {
copyPixels();
}
repaint();
try {
Thread.sleep(40);
}
catch( InterruptedException ie ) {}
}
protected int calcPixelColor( int iterations ) {
int gray = (255*iterations)/MAX_ITERATIONS;
int r = 80 + 5*(gray*gray)/255;
int g = 5*(gray*gray)/255;
int b = gray;
r = Math.min( r, 255 );
g = Math.min( g, 255 );
b = Math.min( b, 255 );
return rgb( r, g, b );
}
protected void renderMandelbrot( float x, float y, float width, float height ) {
int screenWidth = buffer.getWidth();
int screenHeight = buffer.getHeight();
List pixels = new LinkedList();
for ( int i = 0; i < screenWidth; i++ ) {
for ( int j = 0; j < screenHeight; j++ ) {
pixels.add( new Point( i, j ) );
}
}
Collections.shuffle( pixels );
long start = System.currentTimeMillis();
while( !pixels.isEmpty() ) {
Point pt = (Point)pixels.remove(0);
int i = pt.x;
int j = pt.y;
float xi = (x + width*(i/(float)screenWidth));
float yi = (y + height*(j/(float)screenHeight));
int iterations = calcIterations( xi, yi );
int pixel = calcPixelColor( iterations );
setPixel( i, j, pixel );
if ( (System.currentTimeMillis()-start) > 10 ) {
copyPixelsAndPaint();
start = System.currentTimeMillis();
}
}
}
protected void render() {
renderMandelbrot( bounds.x, bounds.y, bounds.width, bounds.height );
synchronized( bufferLock ) {
copyPixels();
}
repaint();
}
public void update(Graphics g) {
paint( g );
}
public void paint( Graphics g ) {
synchronized( bufferLock ) {
if ( buffer != null )
g.drawImage( buffer, 0, 0, null );
}
}
}
Improvements to the code
The code for this series was largely speaking written "live". Which is to say, apart from one snippet, I had only an idea of how the code should work in my head. As of such, there are probably some rough edges, particularly as the code had been developed incrementally and changed over the course of the series. For example by the last episode there are several redundant instance variables and the applet is constantly refreshing/repainting itself - even when no animation is apparent on screen.
Personally I felt that there was no point trying to write perfect code as part of these screencasts. Instead it seemed better to try and demonstrate things clearly, with a view to how one might go about constructing an applet of this nature.
Performance
Rather then creating a new list of points each time renderMandelbrot should create and shuffle the list once:
private List coords = new ArrayList(); // instance variable
protected void renderMandelbrot( float x, float y, float width, float height ) {
int screenWidth = buffer.getWidth();
int screenHeight = buffer.getHeight();
// should normally only do this once
if ( coords.size() != screenWidth*screenHeight ) {
coords.clear(); // ensure it's empty
for ( int i = 0; i < screenWidth; i++ ) {
for ( int j = 0; j < screenHeight; j++ ) {
coords.add( new Point( i, j ) );
}
}
Collections.shuffle( coords ); // Only need to shuffle once.
}
long start = System.currentTimeMillis();
for ( int i = 0; i < coords.size(); i++ ) {
Point pt = (Point) coords.get(i);
int i = pt.x;
int j = pt.y;
float xi = (x + width*(i/(float)screenWidth));
float yi = (y + height*(j/(float)screenHeight));
int iterations = calcIterations( xi, yi );
int pixel = calcPixelColor( iterations );
setPixel( i, j, pixel );
if ( (System.currentTimeMillis()-start) > 10 ) {
copyPixelsAndPaint();
start = System.currentTimeMillis();
}
}
}
Java 5 and Generics
With the release of Java 5, Sun made some changes to the core language. These changes include the for each loop and support for generics. So the renderMandelbrot method can be altered to use both of these and may be deemed as being clearer for it:
// declaration of a list of java.awt.Point
private List<Point> coords = new ArrayList<Point>(); // instance variable
protected void renderMandelbrot( float x, float y, float width, float height ) {
int screenWidth = buffer.getWidth();
int screenHeight = buffer.getHeight();
// should normally only do this once
if ( coords.size() != screenWidth*screenHeight ) {
coords.clear(); // ensure it's empty
for ( int i = 0; i < screenWidth; i++ ) {
for ( int j = 0; j < screenHeight; j++ ) {
coords.add( new Point( i, j ) );
}
}
Collections.shuffle( coords ); // Only need to shuffle once.
}
long start = System.currentTimeMillis();
for ( Point pt: coords ) { // for each pt in coords
int i = pt.x;
int j = pt.y;
float xi = (x + width*(i/(float)screenWidth));
float yi = (y + height*(j/(float)screenHeight));
int iterations = calcIterations( xi, yi );
int pixel = calcPixelColor( iterations );
setPixel( i, j, pixel );
if ( (System.currentTimeMillis()-start) > 10 ) {
copyPixelsAndPaint();
start = System.currentTimeMillis();
}
}
}

