/*====================================================================*\ 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; } //----------------------------------------------------------------------