The Confetti applet is a visual effects demo that is similar to confetti floating through air. If the mouse is off
the applet, the confetti is distributed randomly. When the mouse is on the applet, the confetti follows the mouse spraying
forth like a fountain. Additionally, the confetti inherits the vertical and horizontal momentum of the mouse creating the
illusion of being flung from the mouse location.
The Algorithm
The algorithm is fairly basic. Each peice of confetti is starts with an initial X and Y velocity. Then , the X and Y velocity
of the mouse movement is added to the initial velocity. Once started, the confetti continues movement with its initial velocity.
Gravity is simulated by adding a constant positve Y value to the Y velocity each cycle. This continues until the bottom or side of
the applet is reach. When this happens, the confetti starts all over. The confetti is allowed to extend past the top edge because,
unlike the other three edges, it may eventually come back into the visible area.
Let's Look at Code
Don't worry if my description of the algorithm doesn't make sense. All the secrets are in the code and here it is...
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.awt.image.*;
Here are the imports. java.awt contains the Graphics class and event model. java.awt.event is needed for event model, too.
java.applet is needed, well, because it is an applet after all! And, java.awt.image contains the BufferedImage and DataBuffer classes that
are needed for fast graphics.
public class Confetti extends Applet implements Runnable
Confetti.java is the name of the source file. So, this file needs to have a public class named Confetti. Since I want it
to be an applet, I must extend Applet. Finally, this class implements Runnable. Applets that run continuously, instead of waiting on events,
must run in a thread and implementing the Runnable interface is an easy way create a thread.
{ boolean stop = false;
int Stars = 2500;
int[] StarC = new int[Stars+1];
float[] StarX = new float[Stars+1];
float[] StarY = new float[Stars+1];
float[] StarXA = new float[Stars+1];
float[] StarYA = new float[Stars+1];
float MouseX =0, MouseY =0;
float MouseCX=0, MouseCY=0;
float MouseXA=0, MouseYA=0;
int W, H;
BufferedImage biScreen;
DataBuffer db;
Thread tConfetti;
This is all the variables that are global to this module. I saved some typing but not specifying the type access
priviledges. So, they all default to protected. The first variable, stop, indicates whether to stop the main loop of this thread
or not. Stars is the number of peices of confetti (let's call them stars from now on) that are processed. If your computer is
super fast, try changing this to a super big number. StarC is the color of the star. StarX and StarY are the coordinate location.
StarXA and StarYA are the speed of the star on the X/Y axis. MouseX and MouseY virtual mouse coordinates and MouseCX and MouseCY
are the actual coordinates of the mouse. Virtual coordinates are used we want the effect to be smooth. If the users moves the
mouse fast, the effect would be choppy. By using virtual mouse coordinates, the mouse location can smoothly chases the actual
location. Thus, MouseXA and MouseYA is the speed at which the virtual mouse location is moving. W and H are the width and height
of the applet. biScreen is the image that all the drawing is performed on. db is the buffer that stores the pixel data for the
screen image. And finally, tConfetti stores the reference to the current thread.
public void start()
{ if(tConfetti == null)
{ tConfetti=new Thread(this);
tConfetti.start(); }
}
Confetti implements the runnable interface. To start a Runnable class, there must be a start method. This is
where the thread is started and I used the standard textbook technique to start the thread.
public void stop()
{ stop=true;
tConfetti = null; }
Notice that stop is set to true which causes the main loop to terminate. That is the preferred method now because of
problems with Thread.stop(). Here is what Sun has to say about Thread.Stop(): Stopping a thread with Thread.stop() causes it to unlock
all of the monitors that it has locked (as a natural consequence of the unchecked ThreadDeath exception propagating up the stack).
If any of the objects previously protected by these monitors were in an inconsistent state, the damaged objects become visible to
other threads, potentially resulting in arbitrary behavior. Many uses of stop should be replaced by code that simply modifies some
variable to indicate that the target thread should stop running. The target thread should check this variable regularly, and return
from its run method in an orderly fashion if the variable indicates that it is to stop running.
public void init()
{ this.enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);
W = this.getSize().width;
H = this.getSize().height;
biScreen = new BufferedImage(W, H, 1);
db = biScreen.getRaster().getDataBuffer();
for(int x=0; x<W; x++)
for(int y=0; y<H; y++)
db.setElem(x+(y*W), 0xFFFFFF);
for(int t=0; t<Stars; t++)
newStar(t);
}
For applets, Init is called before run and is the intended location for initializing program variables. Thus, the
first line registers the mouse motion event for further processing. The applet's width and height are then acquired. The BufferedImage
constructor is used to create a new image with the applets width and height as its size. The 1 indicates that the image will be of
type TYPE_INT_RGB. Interestingly, If you changed it to a 2, the image would be TYPE_INT_ARGB. You would think that having an 8 bit
alpha channel would make for great Java demos. However, Java currently only supports hardware acceleration for 1 bit alpha channels.
So, using TYPE_INT_ARGB is painfully slow. Anyhow, db is assigned the reference to biScreen's databuffer. Next is a nested for loop
that clears the screen. Notice that wrote the pixel data directly to the data buffer with the db.setElem. Lastly, we initialize all
the stars.
public void run()
{ while(!stop)
{ MouseXA = (MouseCX-MouseX)/5.0f;
MouseYA = (MouseCY-MouseY)/5.0f;
MouseX +=MouseXA*2.5f;
MouseY +=MouseYA*2.5f;
run is the main routine and it loop until stop is true. The amount of mouse movement is computed and scaled. Then
the virtual mouse location is updated and scaled accordingly. You might think that I should have computed the scaling of MouseX/Y
with MouseXA/YA. However, MouseXA/YA is used in newStar to add mouse momentum to the stars movement. And, the virtual mouse speed
and stars initial momentum needs to be different.
for(int t=0; t<Stars; t++)
{ StarX[t]+=StarXA[t];
StarY[t]+=StarYA[t];
StarYA[t]+=(H/100000.0f);
if((StarY[t]+3>H)||(StarX[t]+3>W)||(StarX[t]-2<0))
newStar(t);
Here, the star's movement is updated and checked against the left, right and bottom screen edges. If the star lies
outside an edge, it is recycled. Also note that the star's YA is always incremented by 1/100000th of the screen height. That causes
the stars to slowly drift down the screen and at a rate proportional to the size of the applet.
if(StarY[t]-1>0)
{ db.setElem((int)StarX[t] + ((int)(StarY[t]+1)*W), StarC[t]);
db.setElem((int)StarX[t] + ((int)(StarY[t]-1)*W), StarC[t]);
db.setElem((int)StarX[t] + ((int)StarY[t]*W)-1, StarC[t]);
db.setElem((int)StarX[t] + ((int)StarY[t]*W)+1, StarC[t]); }
}
Now that the star locations are updated, they may be drawn to the buffer. Each star is made of four pixels that looks
something like a hollow diamond. We want to allow the stars to be flung above the top of the screen. So, any star above the top edge
won't be drawn until it comes back down. Those stars that are not clipped are drawn by writting to the databuffer. setElem(offset, color)
is the method where offset is the offset of the pixel in the buffer and color is the RGB value represented by an int. Remember, the
screen was TYPE_INT_RGB? Well, that means the databuffer will have a single band where each consecutive int represent the color for
each consecutive pixel. Simple, huh?
repaint(0, 0, W, H);
try
{ Thread.sleep(10); }
catch (InterruptedException e)
{;}
Now is a good time to paint the screen and sleep for a bit. Sleeping is a good idea for applets with event handles. It
prevents starving the event handler.
for(int t=0; t<Stars; t++)
if(StarY[t]-1>0)
{ db.setElem((int)StarX[t] + ((int)(StarY[t]+1)*W), 0xFFFFFF);
db.setElem((int)StarX[t] + ((int)(StarY[t]-1)*W), 0xFFFFFF);
db.setElem((int)StarX[t] + ((int)StarY[t]*W)-1, 0xFFFFFF);
db.setElem((int)StarX[t] + ((int)StarY[t]*W)+1, 0xFFFFFF); }
}
}
Before looping back to the beginning, the stars need to be erased. This is easy to do. Just set the stars to the background
color. Now, you might be thinking, "why not just clear the background". Well, I have only 2500 stars, which means 10,000 pixel writes. If my
applet was a modest 640x400, I would need to write 307,200 pixels!
private void newStar(int t)
{ if((MouseX<10)||(MouseX>W-10)||(MouseY>H-10))
{ StarX[t]=(int)(Math.random()*(W-10))+5;
StarY[t]=(int)(Math.random()*(H-10))+5; }
else
{ StarX[t] =(int)MouseX;
StarY[t] =(int)MouseY; }
StarXA[t] = (float)Math.random()*(W/250)-(W/500) + MouseXA;
StarYA[t] = (float)Math.random()*(H/100)-(H/150) + MouseYA;
StarC[t] = ((int)(Math.random()*250)<<16) |
((int)(Math.random()*250)<<8) |
(int)(Math.random()*250);
}
This method resets stars. If the mouse is located 5 pixels within the applet, start the star from the virtual mouse location.
Otherwise, pick a point randomly. Next, set it moving in a random direction and add the momentum from the mouse movement. Finally, pick a random
color for the star. Notice that I generate the red, green and blue separately then combine the bits logically to form an RGB int value.
public void update(Graphics g)
{ paint(g); }
The update method is overridden to prevent the unnecessary clearing of the background.
public void paint(Graphics g)
{ g.drawImage(biScreen, 0,0, this); }
We drew the biScreen image in the run method. So, all we need to do in paint is draw it in this Graphics context.
public void processMouseMotionEvent(MouseEvent e)
{ switch(e.getID())
{ case MouseEvent.MOUSE_DRAGGED:
case MouseEvent.MOUSE_MOVED: MouseCX = (float)e.getX();
MouseCY = (float)e.getY();
} } }
Here is the mouse event handler. For this applet, all I needed was the code to update MouseCX/CY. However, I thought that
someone might want to see how to distinquish between a dragged mouse and a moved mouse. In this case, draggin and moving run the same code.
Thus, the whole switch statement has no effect but to serve as an example.
|