/*====================================================================*\
HelicalLadder.java
Helical ladder applet : main class.
Requires Java Plug-in 1.2 or later.
------------------------------------------------------------------------
HelicalLadder: a trivial animated Java applet.
Copyright 2005 Andy Morgan-Richards.
HelicalLadder is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details:
http://www.gnu.org/licenses/gpl.html
\*====================================================================*/
// IMPORTS
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Random;
import javax.swing.JApplet;
import javax.swing.JComponent;
import javax.swing.Timer;
//----------------------------------------------------------------------
// HELICAL LADDER APPLET : MAIN CLASS
public class HelicalLadder
extends JApplet
implements ActionListener
{
////////////////////////////////////////////////////////////////////////
// Constants
////////////////////////////////////////////////////////////////////////
// Applet names and version
public static final String SHORT_NAME = "HelicalLadder";
public static final String LONG_NAME = "Helical Ladder";
public static final int VERSION_MAJOR = 0;
public static final int VERSION_MINOR = 0;
public static final String VERSION_BUILD = "";
// Applet info
private static final String APPLET_STR = "Applet: ";
private static final String APPLET_INFO_STR =
"Title: " + LONG_NAME + " " + VERSION_MAJOR + "." + VERSION_MINOR + "\n" +
"Author: Andy Morgan-Richards\n" +
"A trivial animated applet.";
// Applet parameters
private static final String PARAM_PREFIX = "app.";
private interface ParamName
{
String APPLET_TYPE = "appletType";
String COLOUR = "colour";
String DIRECTION = "direction";
String INSTANCE = "instance";
}
// Applet type
private interface Type
{
int MONODIRECTIONAL = 0;
int BIDIRECTIONAL = 1;
}
private static final String[] TYPE_STRS = { "monodirectional", "bidirectional" };
// Icon and image dimensions
private static final int ICON_WIDTH = 34;
private static final int ICON_HEIGHT = 32;
private static final int NUM_ICONS = 20;
private static final int IMAGE_WIDTH = ICON_WIDTH;
private static final int IMAGE_HEIGHT = NUM_ICONS * ICON_HEIGHT;
// Icon definition
private static final String[] ICON_DEFINITION =
{
"0222200000000000110000000000033330",
"0222200000000000110000000000033330",
"0022220000000000110000000000333300",
"0002222000000000110000000003333000",
"0000222200000000110000000033330000",
"0000002222000000110000003333000000",
"0000000022220000000000333300000000",
"0000000000000000333333300000000000",
"0000000000033333330000000000000000",
"0000000033330000000000222200000000",
"0000003333000000110000002222000000",
"0000333300000000110000000022220000",
"0003333000000000110000000002222000",
"0033330000000000110000000000222200",
"0333300000000000110000000000022220",
"0333300000000000110000000000022220",
"0333300000000000110000000000022220",
"0333300000000000110000000000022220",
"0033330000000000110000000000222200",
"0003333000000000110000000002222000",
"0000333300000000110000000022220000",
"0000003333000000110000002222000000",
"0000000033330000000000222200000000",
"0000000000000000222222200000000000",
"0000000000022222220000000000000000",
"0000000022220000000000333300000000",
"0000002222000000110000003333000000",
"0000222200000000110000000033330000",
"0002222000000000110000000003333000",
"0022220000000000110000000000333300",
"0222200000000000110000000000033330",
"0222200000000000110000000000033330",
};
// Sprite colours
private static final Color[] SPRITE_COLOURS_MONODIRECTIONAL =
{
new Color( 64, 64, 64 ), // dark grey
new Color( 200, 0, 0 ), // red
new Color( 0, 200, 0 ), // green
new Color( 0, 0, 200 ), // blue
new Color( 200, 200, 0 ), // yellow
new Color( 200, 0, 200 ), // magenta
new Color( 0, 200, 200 ), // cyan
new Color( 248, 144, 112 ) // orange
};
private static final Color[] SPRITE_COLOURS_BIDIRECTIONAL =
{
new Color( 0, 176, 176 ),
new Color( 176, 0, 176 )
};
// Sprite constants
private interface Direction
{
int UP = 0;
int DOWN = 1;
}
private static final String[] DIRECTION_STRS = { "up", "down" };
private static final int MIN_SPRITE_DELAY = 6;
private static final int MAX_SPRITE_DELAY = 18;
private static final SpritePosition[] SPRITE_POSITIONS =
{
new SpritePosition( 3, 6.0 ),
new SpritePosition( 6, 5.5 ),
new SpritePosition( 0, 0.0 ),
new SpritePosition( 28, 5.5 ),
new SpritePosition( 31, 6.0 ),
new SpritePosition( 28, 6.5 ),
new SpritePosition( 17, 7.0 ),
new SpritePosition( 6, 6.5 )
};
private static final int NUM_SPRITE_POSITIONS = SPRITE_POSITIONS.length;
private static final int SPRITE_DELTA_Y = ICON_HEIGHT / NUM_SPRITE_POSITIONS;
private static final int MIN_SPRITE_Y_SEPARATION = MIN_SPRITE_DELAY * SPRITE_DELTA_Y;
private static final int MAX_SPRITE_SIZE = 7;
// Image update interval, milliseconds
private static final int UPDATE_INTERVAL = 100;
// Default values
private static final int DEFAULT_TYPE = Type.MONODIRECTIONAL;
private static final int DEFAULT_LADDER_DIRECTION = Direction.DOWN;
private static final int[] DEFAULT_LADDER_RGB_VALUES =
{
0xFFFFFF, // '0'
0x909090, // '1'
0xD0D0D0, // '2'
0xC6C6C6 // '3'
};
// Error messages
private static final String ILLEGAL_PARAMETER_VALUE_STR = "Illegal parameter value: ";
////////////////////////////////////////////////////////////////////////
// Member classes : non-inner classes
////////////////////////////////////////////////////////////////////////
// SPRITE CLASS
private static class Sprite
{
////////////////////////////////////////////////////////////////////
// Constructors
////////////////////////////////////////////////////////////////////
private Sprite( int pos,
int y,
int direction,
Color colour )
{
this.pos = pos;
this.y = y;
this.direction = direction;
this.colour = colour;
}
//--------------------------------------------------------------
////////////////////////////////////////////////////////////////////
// Instance variables
////////////////////////////////////////////////////////////////////
int pos;
int y;
int direction;
Color colour;
}
//==================================================================
// SPRITE POSITION CLASS
private static class SpritePosition
{
////////////////////////////////////////////////////////////////////
// Constructors
////////////////////////////////////////////////////////////////////
public SpritePosition( int x,
double size )
{
this.x = x;
this.size = size;
}
//--------------------------------------------------------------
////////////////////////////////////////////////////////////////////
// Instance variables
////////////////////////////////////////////////////////////////////
int x;
double size;
}
//==================================================================
////////////////////////////////////////////////////////////////////////
// Member classes : inner classes
////////////////////////////////////////////////////////////////////////
// MAIN PANEL CLASS
private class MainPanel
extends JComponent
{
////////////////////////////////////////////////////////////////////
// Constructors
////////////////////////////////////////////////////////////////////
private MainPanel( )
{
setOpaque( true );
}
//--------------------------------------------------------------
////////////////////////////////////////////////////////////////////
// Instance methods : overriding methods
////////////////////////////////////////////////////////////////////
/**
* Paints the helical ladder image and sprites in the main panel.
*
* @param gr the {@code Graphics} object that provides the graphics context for the paint
* operation.
* @see javax.swing.JComponent#paintComponent(java.awt.Graphics)
*/
protected void paintComponent( Graphics gr )
{
// Set the image data in the buffered image
image.setRGB( 0, 0, IMAGE_WIDTH, IMAGE_HEIGHT, imageBuffer, IMAGE_WIDTH * imageOffset,
IMAGE_WIDTH );
// Draw the active sprites in the buffered image
Graphics2D imageGr = image.createGraphics( );
imageGr.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
ListIterator it = sprites.listIterator( );
while ( it.hasNext( ) )
{
Sprite sprite = (Sprite)it.next( );
double size = SPRITE_POSITIONS[sprite.pos].size;
if ( size != 0.0 )
{
double halfSize = size * 0.5;
imageGr.setColor( sprite.colour );
imageGr.fill( new Ellipse2D.Double( (double)SPRITE_POSITIONS[sprite.pos].x - halfSize,
(double)sprite.y - halfSize, size, size ) );
}
}
// Draw the buffered image
gr.drawImage( image, 0, 0, null );
}
//--------------------------------------------------------------
}
//==================================================================
////////////////////////////////////////////////////////////////////////
// Constructors
////////////////////////////////////////////////////////////////////////
public HelicalLadder( )
{
}
//------------------------------------------------------------------
////////////////////////////////////////////////////////////////////////
// Class methods
////////////////////////////////////////////////////////////////////////
/**
* Returns the index of the first occurrence of an object in an array of objects. The objects must be
* capable of being tested for equality with {@code java.lang.Object.equals(Object)}.
*
* @param values the array of objects that is to be searched.
* @param target the target object.
* @return the index of the target object in the array, or {@code -1} if the array does not contain the
* target object.
*/
public static int getIndex( Object[] values,
Object target )
{
for ( int i = 0; i < values.length; ++i )
{
if ( values[i].equals( target ) )
return i;
}
return -1;
}
//------------------------------------------------------------------
/**
* Writes to the standard error stream the name and value of an applet parameter whose value is illegal.
*
* @param name the name of the parameter whose value is illegal.
* @param value the value of the parameter.
*/
private static void printIllegalParam( String name,
String value )
{
System.err.println( ILLEGAL_PARAMETER_VALUE_STR + name + " = " + value );
}
//------------------------------------------------------------------
////////////////////////////////////////////////////////////////////////
// Instance methods : ActionListener interface
////////////////////////////////////////////////////////////////////////
/**
* Updates the main panel when an event from the image update timer is received.
*
* @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
*/
public void actionPerformed( ActionEvent event )
{
// Decrement image offset to shift ladder
int ladderIncrement = (ladderDirection == Direction.UP) ? 1 : -1;
imageOffset += ladderIncrement;
if ( imageOffset < 0 )
imageOffset += ICON_HEIGHT;
if ( imageOffset >= ICON_HEIGHT )
imageOffset -= ICON_HEIGHT;
// Move sprites on ladder
ListIterator it = sprites.listIterator( );
while ( it.hasNext( ) )
{
Sprite sprite = (Sprite)it.next( );
switch ( sprite.direction )
{
case Direction.UP:
{
// Decrement the y coordinate of the sprite
sprite.y -= SPRITE_DELTA_Y;
sprite.y -= ladderIncrement;
// If the sprite has reached the top of the ladder, remove it from the list ...
if ( sprite.y < -(MAX_SPRITE_SIZE >> 1) )
it.remove( );
// ... otherwise, decrement the position of the sprite
else
{
if ( --sprite.pos < 0 )
sprite.pos += NUM_SPRITE_POSITIONS;
}
break;
}
case Direction.DOWN:
{
// Increment the y coordinate of the sprite
sprite.y += SPRITE_DELTA_Y;
sprite.y -= ladderIncrement;
// If the sprite has reached the bottom of the ladder, remove it from the list ...
if ( sprite.y >= IMAGE_HEIGHT + (MAX_SPRITE_SIZE >> 1) )
it.remove( );
// ... otherwise, increment the position of the sprite
else
{
if ( ++sprite.pos >= NUM_SPRITE_POSITIONS )
sprite.pos -= NUM_SPRITE_POSITIONS;
}
break;
}
}
}
// If the delay since creating the previous sprite has elapsed ...
if ( --spriteDelay < 0 )
{
// Add a new sprite to the list
int y = ((spriteDirection == Direction.DOWN) ? SPRITE_DELTA_Y : IMAGE_HEIGHT)
- imageOffset % SPRITE_DELTA_Y;
switch ( type )
{
case Type.MONODIRECTIONAL:
addSprite( y );
break;
case Type.BIDIRECTIONAL:
addSprite( y, spriteDirection );
spriteDirection ^= 1;
break;
}
// Set the delay until the next sprite is created
spriteDelay = MIN_SPRITE_DELAY + randSeq.nextInt( MAX_SPRITE_DELAY - MIN_SPRITE_DELAY + 1 );
}
// Redraw the main panel
mainPanel.repaint( );
}
//------------------------------------------------------------------
////////////////////////////////////////////////////////////////////////
// Instance methods : overriding methods
////////////////////////////////////////////////////////////////////////
/**
* Initialises the applet.
*
* @see java.applet.Applet#init()
*/
public void init( )
{
// Print the applet's name
System.out.println( APPLET_STR + SHORT_NAME );
// Initialise the random sequence using the applet instance parameter in the seed
int instanceIndex = 0;
String paramValue = getParameter( ParamName.INSTANCE );
if ( paramValue != null )
{
try
{
instanceIndex = Integer.parseInt( paramValue );
}
catch ( NumberFormatException e )
{
printIllegalParam( ParamName.INSTANCE, paramValue );
}
}
randSeq = new Random( System.currentTimeMillis( ) +
Double.doubleToLongBits( Math.PI * (double)instanceIndex ) );
// Get the applet type
type = DEFAULT_TYPE;
paramValue = getParameter( ParamName.APPLET_TYPE );
if ( paramValue != null )
{
int value = getIndex( TYPE_STRS, paramValue.toLowerCase( ) );
if ( value < 0 )
printIllegalParam( ParamName.APPLET_TYPE, paramValue );
else
type = value;
}
// Get the ladder direction
ladderDirection = DEFAULT_LADDER_DIRECTION;
paramValue = getParameter( ParamName.DIRECTION );
if ( paramValue != null )
{
int value = getIndex( DIRECTION_STRS, paramValue.toLowerCase( ) );
if ( value < 0 )
printIllegalParam( ParamName.DIRECTION, paramValue );
else
ladderDirection = value;
}
// Set the sprite direction opposite to the ladder direction
spriteDirection = ladderDirection ^ 1;
// Initialise the RGB values of the ladder icon
int[] rgbValues = new int[DEFAULT_LADDER_RGB_VALUES.length];
System.arraycopy( DEFAULT_LADDER_RGB_VALUES, 0, rgbValues, 0, DEFAULT_LADDER_RGB_VALUES.length );
for ( int i = 0; i < rgbValues.length; ++i )
{
paramValue = getParameter( ParamName.COLOUR + i );
if ( paramValue != null )
{
try
{
if ( !paramValue.startsWith( "#" ) || (paramValue.length( ) != 7) )
throw new NumberFormatException( );
int rgb = Integer.parseInt( paramValue.substring( 1 ), 16 );
rgbValues[i] = rgb;
}
catch ( NumberFormatException e )
{
printIllegalParam( ParamName.COLOUR + i, paramValue );
}
}
}
// Initialise the image data
int numDestRows = (NUM_ICONS + 1) * ICON_HEIGHT;
imageBuffer = new int[IMAGE_WIDTH * numDestRows];
int srcRow = 0;
for ( int destRow = 0; destRow < numDestRows; ++destRow )
{
String str = ICON_DEFINITION[srcRow];
for ( int column = 0; column < str.length( ); ++column )
imageBuffer[destRow * IMAGE_WIDTH + column] = rgbValues[str.charAt( column ) - '0'];
if ( ++srcRow >= ICON_HEIGHT )
srcRow = 0;
}
image = new BufferedImage( IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB );
// Set the image offset to a random value
imageOffset = randSeq.nextInt( ICON_HEIGHT );
// Initialise the sprites
spriteColourIndexes = new int[SPRITE_COLOURS_MONODIRECTIONAL.length];
prevSpriteColourIndex = -1;
sprites = new LinkedList( );
int y = SPRITE_DELTA_Y - imageOffset % SPRITE_DELTA_Y;
while ( true )
{
y += MIN_SPRITE_Y_SEPARATION + randSeq.nextInt( MAX_SPRITE_DELAY - MIN_SPRITE_DELAY + 1 ) *
SPRITE_DELTA_Y;
if ( y >= IMAGE_HEIGHT - MIN_SPRITE_Y_SEPARATION )
break;
switch ( type )
{
case Type.MONODIRECTIONAL:
addSprite( y );
break;
case Type.BIDIRECTIONAL:
addSprite( y, randSeq.nextInt( 2 ) );
break;
}
}
// Create the main panel
mainPanel = new MainPanel( );
setContentPane( mainPanel );
// Start the update timer
timer = new Timer( UPDATE_INTERVAL, this );
timer.start( );
}
//------------------------------------------------------------------
/**
* Starts execution of the applet.
*
* @see java.applet.Applet#start()
*/
public void start( )
{
if ( timer != null )
timer.restart( );
}
//------------------------------------------------------------------
/**
* Stops execution of the applet.
*
* @see java.applet.Applet#stop()
*/
public void stop( )
{
if ( timer != null )
timer.stop( );
}
//------------------------------------------------------------------
/**
* Returns information about the applet.
*
* @return a string containing information about the applet.
* @see java.applet.Applet#getAppletInfo()
*/
public String getAppletInfo( )
{
return APPLET_INFO_STR;
}
//------------------------------------------------------------------
/**
* Returns the value of the parameter with the specified name in the HTML element in which the applet
* was included.
*
* @param name the name of the parameter whose value is to be returned.
* @return the value of the named parameter, or {@code null} if the parameter is not set.
* @see java.applet.Applet#getParameter(java.lang.String)
*/
public String getParameter( String name )
{
return super.getParameter( PARAM_PREFIX + name );
}
//------------------------------------------------------------------
////////////////////////////////////////////////////////////////////////
// Instance methods
////////////////////////////////////////////////////////////////////////
/**
* Returns the position index of a sprite.
*
* @param y the y coordinate of the sprite.
* @param alternateHelix position the sprite on the alternate helix.
* @return the position index of a sprite at the specified y coordinate on the specified helix.
*/
private int getSpritePosition( int y,
boolean alternateHelix )
{
int pos = (y + imageOffset) % ICON_HEIGHT / SPRITE_DELTA_Y;
if ( alternateHelix )
{
pos -= NUM_SPRITE_POSITIONS >> 1;
if ( pos < 0 )
pos += NUM_SPRITE_POSITIONS;
}
return pos;
}
//------------------------------------------------------------------
/**
* Creates a new sprite on a monodirectional ladder and adds it to the list of sprites.
*
* @param y the y coordinate of the sprite.
* @see #addSprite(int, int)
*/
private void addSprite( int y )
{
// If the array of sprite colour indexes is empty, fill it
if ( numSpriteColourIndexes == 0 )
{
for ( int i = 0; i < SPRITE_COLOURS_MONODIRECTIONAL.length; ++i )
spriteColourIndexes[i] = i;
numSpriteColourIndexes = SPRITE_COLOURS_MONODIRECTIONAL.length;
}
// Get the colour index of the new sprite
int colourIndex = 0;
while ( true )
{
int i = randSeq.nextInt( numSpriteColourIndexes );
colourIndex = spriteColourIndexes[i];
if ( prevSpriteColourIndex != colourIndex )
{
prevSpriteColourIndex = colourIndex;
spriteColourIndexes[i] = spriteColourIndexes[--numSpriteColourIndexes];
break;
}
}
// Get the sprite position index: on half the occasions, put the sprite on the "other" helix
int pos = getSpritePosition( y, randSeq.nextInt( 2 ) == 0 );
// Add a new sprite to the list
sprites.add( new Sprite( pos, y, spriteDirection, SPRITE_COLOURS_MONODIRECTIONAL[colourIndex] ) );
}
//------------------------------------------------------------------
/**
* Creates a new sprite on a bidirectional ladder and adds it to the list of sprites.
*
* @param y the y coordinate of the sprite.
* @param direction the direction of motion of the sprite.
* @see #addSprite(int)
*/
private void addSprite( int y,
int direction )
{
// Get the sprite position index: if the sprite's direction is down, put it on the "other" helix
int pos = getSpritePosition( y, direction == Direction.DOWN );
// Add a new sprite to the list
sprites.add( new Sprite( pos, y, direction, SPRITE_COLOURS_BIDIRECTIONAL[direction] ) );
}
//------------------------------------------------------------------
////////////////////////////////////////////////////////////////////////
// Instance variables
////////////////////////////////////////////////////////////////////////
private int type;
private MainPanel mainPanel;
private Random randSeq;
private Timer timer;
private BufferedImage image;
private int[] imageBuffer;
private int imageOffset;
private int ladderDirection;
private LinkedList sprites;
private int spriteDelay;
private int spriteDirection;
private int[] spriteColourIndexes;
private int numSpriteColourIndexes;
private int prevSpriteColourIndex;
}
//----------------------------------------------------------------------