Swing Hack 7: Let it Snow!

I've always wanted to make some sort of display that draws outside of a window with images scattered all across the screen. Though previously impossible in Java, I found a way to do it. And since Christmas is coming up I thought I'd use it to make a snowflake display. Here's a cropped screenshot of what it looks like:

Snowfall Screenshot (click for fullsize)

Okay. I lied. I didn't really find a way to draw outside of a JFrame. It's impossible without native code. However, we can make a really nice fake. While searching on the internet (I love how anything can be explained with 'while I was searching on the internet') I came across an example of making transparent windows. It seems that the Robot class has the ability to take a screen capture. Combined with fullscreen mode we can pretend that we have the ability to draw all over the screen outside of frame boundaries.

Here's the basic idea: Take a screenshot. Switch to fullscreen mode. Make a panel that covers the whole screen. Draw the screenshot onto the panel. Draw what we want on top of the panel. Voilia, we can pretend to draw anywhere on the screen.

This is longer than any of my previous hacks, so I'll go through just the important bits and then let you download the code.

This makes a screen capture:

public static void captureScreen() throws AWTException {
Robot robot = new Robot();
Toolkit tk = Toolkit.getDefaultToolkit();
size = tk.getScreenSize();
Rectangle bounds = new Rectangle(0,0,(int)size.getWidth(),(int)size.getHeight());
screen = robot.createScreenCapture(bounds);
}

This switches to full screen mode:

public static JFrame goFullScreen() {
GraphicsEnvironment env = GraphicsEnvironment.
getLocalGraphicsEnvironment();
device = env.getDefaultScreenDevice();
original_mode = device.getDisplayMode();
JFrame frame = new JFrame(device.getDefaultConfiguration());
frame.setUndecorated(true);
frame.setResizable(false);
device.setFullScreenWindow(frame);
return frame;
}

This switches back to normal mode:

public static void goNormalScreen() {
device.setDisplayMode(original_mode);
device.setFullScreenWindow(null);
}

This initializes all of the snowflake variables with random values. It lets us control how far away the flakes are, how fast they fall, and how much they twitter from side to side.

    points = new Point[num_flakes];
pointimages = new Image[num_flakes];
rates = new int[num_flakes];
distances = new int[num_flakes];
for(int i=0; i<points.length; i++) {
// create a new point
points[i] = new Point(-1,-1);
// set a random distance
distances[i] = 1+(int)(Math.random()*max_distance);
// set the fallrate to be inversely proportional to the distance
rates[i] = 1+(int)(fall_rate/distances[i]);
// randomly choose one of the images;
pointimages[i] = snowflake_source[(int)(Math.random()*8)];
}

This runs in a separate thread to update each snowflake. We respawn only on every 10th loop, and only if there is a slot free for a new flake. Maybe this would be better with growable vectors or a queue.

public void run() {
go = true;
int spawncount = 0;
while(go) {
spawncount++;
// sleep for 100 milliseconds
try { Thread.sleep(20); } catch (Exception ex) { }
// loop over each point and move it if appropriate
for(int i=0; i<points.length; i++) {
Point pt = points[i];
// if it's a dead snowflake
if(pt.y <0 ) {
// only respawn on every 10th time through the loop
if(spawncount%10==0) {
pt.y = 10;
pt.x = (int) (Math.random() * size.getWidth());
spawncount++;
}
continue;
}

// if at the bottom of the screen the kill it
if(pt.y > screen.getHeight()) {
pt.y = -1;
continue;
}

// for all normal snowflakes
// move each point down
points[i].y = points[i].y + rates[i];
// move randomly to the right or left
points[i].x += (int)(5*(0.5-Math.random()));
}
// respawn it if needed
repaint();
}
}

This does the actual painting.

public void paintComponent(Graphics g) {
g.drawImage(screen,0,0,null);

for(int i=0; i<points.length; i++) {
if(points[i].y > 0) {
double w= pointimages[i].getWidth(null);
double h = pointimages[i].getHeight(null);
// dist = 0 w = w*1
// dist = 20 w = w*1/20
w = w * 1/this.distances[i];
h = h * 1/this.distances[i];
g.drawImage(pointimages[i], points[i].x+20, points[i].y+20, (int)w, (int)h ,null);
}
}
}

I'm sure you could all imagine more realistic algorithms for doing it, but this will just draw the flakes larger and smaller based on the distance. If we wanted to we could start using affine transforms to make the flakes spin and rotate.

Here's a zipfile with the code and images.

Happy Holidays everyone. I'm out until next year!

Talk to me about it on Twitter

Posted December 22nd, 2003

Tagged: java swing-hacks java.net