Skip to main content

Swing Hack 7: Let it Snow!

Posted by joshy on December 22, 2003 at 6:11 AM PST

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!