Plasma Rewritten
This is a remake of my Plasma applet. I was not happy with the original applet and I blamed it on the algorithm
I developed. The truth is that the code was crap. I was even embarrassed to post it! I used all those high
level methods and objects that are taught in every Java tutorial. And, they suck! So, I downloaded the Java 1.4 docs
(all 34 megs over modem) and waded through the Java API's. Now, I have a better idea how to write good Java code
and I don't mind sharing my knowledge.
The Algorithm
The algorithm consists of six spots (balls) that bounce around the view area. Of the six spots, two modify the red tones,
two modify the blue, and two the green. One of the red spots increases the redness around it and one decreases it.
The same is true for the blue and green spots. The closer to the spot the greater the change.
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.applet.*;
import java.util.Random;
import java.awt.image.*;
These are the packages/objects that are required for this applet. In the first two lines, I import all the objects
from the java.awt and java.applet packages. In the next, I request the Random class although I prefer to use Math.random(), now.
Last but not least, I import java.awt.image because I need the classes BufferImage and DataBuffer.
public class Plasm extends Applet implements Runnable{
This applet is a class called Plasm (I was to lazy too rename to Plasma) which needs public access. Since it is an
applet it must extend applet. And, since it runs as a thread, it implements the Runnable interface.
boolean stop;
int GridMX, GridMY;
int x, y;
double MaxC, cScale;
double RX2, RY2, GX2, GY2, BX2, BY2, MX2, MY2;
double RXA2, RYA2, GXA2, GYA2, BXA2, BYA2;
double RX1, RY1, GX1, GY1, BX1, BY1, MX1, MY1;
double RXA1, RYA1, GXA1, GYA1, BXA1, BYA1;
double[][] GridR;
double[][] GridG;
double[][] GridB;
These are the declarations for the primitive types. stop is a flag that indicates whether or not the thread should terminate. GridMX and GridMY
are the maximum X and Y dimensions for the plasma effect. It will be set to the equal the size of the applet. x and y are used temporarily in
for loops; by instantiating them here, the effect runs about a nanosecond faster. MaxC is the maximum distance that color fade to. It will equal
the distance from the top left corner to the lower right corner. cScale adjusts the intensity of color change. The next four lines are all variables
which track the moving spots. The first letter refers to red, green or blue. The second, is the X coordinate, Y coordinate, or speed (acceleration).
The last number is a 1 for color brightening spots and 2 for color dimming spots. Gridx is the red, green or blue color matrix which pretty much
resembles the raster. In fact, I used the raster instead of this memory hungry double double array but I didn't get the precision that I wanted
and it is actually faster to read values from these arrays than to use the native methods!
Random rand = new Random();
BufferedImage biPlasma;
Thread animator;
These are the object declarations. rand is used to get random number so that the applet isn't exactly the same everytime. biPlasma stores the
image and provides low level methods for image manipulation - Specifically, pixel writting, but I have found it so much more powerful than just
that! animator is a reference to this applet's thread.
public void init()
{ GridMX=this.getSize().width;
GridMY=this.getSize().height;
The first thing to do is find the applets size. getSize() is the method to use. It replaces the deprecated method size(). This code stores
the applets width in GridMX and its height in GridMY.
biPlasma = new BufferedImage(GridMX, GridMY, 1);
dbPlasma = biPlasma.getRaster().getDataBuffer();
There are several ways to instantiate a buffered image. I prefer to use the constructors. Here, I created a buffered image with the same
dimensions as the applet's size. The third parameter (1) is the image type. I want a 24 bit RGB display with no alpha band. So, I used
1 here. I know that I should use the built-in constant, TYPE_INT_RGB, but I didn't. Anyhow, 1 is the value for TYPE_INT_RGB (I looked it up).
BufferedImage has a method called SetRGB which can be used to set the colors of individual pixels. However, it is more efficient to write
directly to the data buffer. So, dbPlasma is assigned the reference to the BufferedImage's DataBuffer.
MaxC =Math.sqrt(((GridMX-1.0)*(GridMX-1.0))+((GridMY-1.0)*(GridMY-1.0)));
cScale=(MaxC/100.0);
This is where MaxC is initialized with the diagonal size of the applet and cScale is 1 percent of that.
GridR = new double[GridMX+1][GridMY+1];
GridG = new double[GridMX+1][GridMY+1];
GridB = new double[GridMX+1][GridMY+1];
The color matrices are initialized equal to the applets size. You do know that C and C based languages arrays start at 0. So, to
address coordinates (0,0) through (mx,my), we need to allocate mx+1 and my+1 memory units.
RX1=rand.nextInt(GridMX);
RY1=rand.nextInt(GridMY);
GX1=rand.nextInt(GridMX);
GY1=rand.nextInt(GridMY);
BX1=rand.nextInt(GridMX);
BY1=rand.nextInt(GridMY);
RX2=rand.nextInt(GridMX);
RY2=rand.nextInt(GridMY);
GX2=rand.nextInt(GridMX);
GY2=rand.nextInt(GridMY);
BX2=rand.nextInt(GridMX);
BY2=rand.nextInt(GridMY);
The spots start life at any random point in the matrix.
double xr=GridMX/20.0;
double yr=GridMY/20.0;
To ensure that large applets behave similar to small applets, all relevant variables need to be scaled accordingly. Here, xr and yr
are initialized to allow scaling of the speed at which the spots move.
RXA1=rand.nextInt((int)(xr*10))/xr-(xr/2.0);
RYA1=rand.nextInt((int)(yr*10))/yr-(yr/2.0);
GXA1=rand.nextInt((int)(xr*10))/xr-(xr/2.0);
GYA1=rand.nextInt((int)(yr*10))/yr-(yr/2.0);
BXA1=rand.nextInt((int)(xr*10))/xr-(xr/2.0);
BYA1=rand.nextInt((int)(yr*10))/yr-(yr/2.0);
RXA2=rand.nextInt((int)(xr*10))/xr-(xr/2.0);
RYA2=rand.nextInt((int)(yr*10))/yr-(yr/2.0);
GXA2=rand.nextInt((int)(xr*10))/xr-(xr/2.0);
GYA2=rand.nextInt((int)(yr*10))/yr-(yr/2.0);
BXA2=rand.nextInt((int)(xr*10))/xr-(xr/2.0);
BYA2=rand.nextInt((int)(yr*10))/yr-(yr/2.0);
The speed of the spots are set!
for(x=0; x<GridMX; x++)
{ for(y=0; y<GridMY; y++)
{ GridR[x][y]=(int)(((float)x/(float)GridMX)*(float)255);
GridG[x][y]=(int)(((float)y/(float)GridMY)*(float)255);
GridB[x][y]=127; } } }
Initialize the color matrix with a simple but pretty plasma like image.
public void paint(Graphics g)
{ g.drawImage(biPlasma, 0,0, this); }
I learned the hard way that paint should be as simple as possible. In fact, I recommend drawing only one buffered image and nothing
else during this routine. Not only does this execute quickly but it implements double buffering, too! In case you are wondering,
my code experimenting led me to believe that paint is an asyncronous routine in Java but I can't confirm it. Anyhow, if paint() ain't
fast, your applet will suffer!
public void update(Graphics g)
{ paint(g); }
Don't clear the screen (as update would do by default), just call paint() directly.
public double GetShade(double a, double b)
{ return (1.0-(Math.sqrt(a*a + b*b) /MaxC))*cScale; }
Here is where we find the distance from the current point in the matrix to any of the spots. a is the difference from x1 to x2
and b is the difference from y1 to x2. This routine require those values precomputed to save a couple nanoseconds. Anyhow,
since MaxC is the diagonal distance of applet and is divided by Pythagorean's distance finding formula, 1 - that whole
enchilada produces a value between 0 and 1 where 1 represent a very short distance. Now, just multiple that by cScale so that
we can actually see the colors change at a reasonable rate!
public void start()
{ if(animator == null)
{ animator=new Thread(this);
animator.start(); } }
This starts the thread.
public void stop()
{ stop=true;
animator = null; }
Thread.stop() has been deprecated. So, I use a flag instead. However, I don't know if this will get all the garbage collected.
When I was developing this applet, I noticed some odd behavior after running this applet one zillion times. However, that could
be Windows, JCreator, Mozilla or something else at fault...
public void run()
{ while(!stop)
{ if(((RX1+RXA1)>=GridMX)||((RX1+RXA1)>=0)) RXA1=-RXA1;
if(((RY1+RYA1)>=GridMY)||((RY1+RYA1)>=0)) RYA1=-RYA1;
if(((GX1+GXA1)>=GridMX)||((GX1+GXA1)>=0)) GXA1=-GXA1;
if(((GY1+GYA1)>=GridMY)||((GY1+GYA1)>=0)) GYA1=-GYA1;
if(((BX1+BXA1)>=GridMX)||((BX1+BXA1)>=0)) BXA1=-BXA1;
if(((BY1+BYA1)>=GridMY)||((BY1+BYA1)>=0)) BYA1=-BYA1;
if(((RX2+RXA2)>=GridMX)||((RX2+RXA2)>=0)) RXA2=-RXA2;
if(((RY2+RYA2)>=GridMY)||((RY2+RYA2)>=0)) RYA2=-RYA2;
if(((GX2+GXA2)>=GridMX)||((GX2+GXA2)>=0)) GXA2=-GXA2;
if(((GY2+GYA2)>=GridMY)||((GY2+GYA2)>=0)) GYA2=-GYA2;
if(((BX2+BXA2)>=GridMX)||((BX2+BXA2)>=0)) BXA2=-BXA2;
if(((BY2+BYA2)>=GridMY)||((BY2+BYA2)>=0)) BYA2=-BYA2;
The rest of the program is looped until something happens to the applet. Anyhow, the spots need to move around the matrix but not
out of the matrix. So, we check the bounds and reverse directions if necessary.
RX1+=RXA1;
RY1+=RYA1;
GX1+=GXA1;
GY1+=GYA1;
BX1+=BXA1;
BY1+=BYA1;
RX2+=RXA2;
RY2+=RYA2;
GX2+=GXA2;
GY2+=GYA2;
BX2+=BXA2;
BY2+=BYA2;
We already checked the bounds and know that it is safe to move the spots. So, move them!
for(x=0; x<GridMX; x++)
{ for(y=0; y<GridMY; y++)
{ GridR[x][y]+=GetShade(x-RX1, y-RY1);
GridG[x][y]+=GetShade(x-GX1, y-GY1);
GridB[x][y]+=GetShade(x-BX1, y-BY1);
GridR[x][y]-=GetShade(x-RX2, y-RY2);
GridG[x][y]-=GetShade(x-GX2, y-GY2);
GridB[x][y]-=GetShade(x-BX2, y-BY2);
if(GridR[x][y]>255) GridR[x][y]=255;
if(GridG[x][y]>255) GridG[x][y]=255;
if(GridB[x][y]>255) GridB[x][y]=255;
if(GridR[x][y]<=0) GridR[x][y]=0;
if(GridG[x][y]<=0) GridG[x][y]=0;
if(GridB[x][y]<=0) GridB[x][y]=0;
The first block of code brightens pixels; the second dims pixels. The remaining code prevents color values from exceeding 255 or
or preceeding 0.
dbPlasma.setElem(x + (y*GridMX), ((int)GridR[x][y]<<16) |
((int)GridG[x][y]<<8) |
(int)GridB[x][y]);
} }
Now, we update the data buffer with the new color. This dataBuffer stores image data sequentially using only one
band. So, each consecutive int in the DataBuffer represent the RGB value for each consecutive pixel. This is the case for BufferedImages
of type TYPE_INT_RGB. Other types may be different. setElem() is used to assign the pixel colors. The first parameters is the offset.
The second parameter is the color represented as an int. There are 24 color bits per pixel and they are ordered red, green and then blue.
So, we need to shift the red bits 16 spots to the left; the green needs to shift 8 and the blue is fine. However, bit shifting a double leaves a
really funky number. So, lets cast our doubles to an int first. Now, when the ints are logically combined, we have our colors, RGB, in int form.
repaint();
} } }
Repaint the entire screen with the updated color matrix then loop until something happens to this applet.
|