The holiday season is once more upon us. This festive time should be joyful, as families get together for fun and celebration. This installment of Java Fun and Games should help get you in the holiday spirit. It presents an applet that focuses on the classic 1945 Christmas song by lyricist Sammy Cahn and composer Jule Styne: "Let It Snow! Let It Snow! Let It Snow!"

The LetItSnow applet animates snowflakes falling past and behind the lyrics to "Let It Snow! Let It Snow! Let It Snow!" Because several variations of this song's lyrics can be found on the Internet, I am not certain if the lyrics I present exactly match the original lyrics. Regardless, you can read the lyrics and watch the snowflakes fall while listening to a MIDI (Musical Instrument Digital Interface) rendition of the song. Just position the mouse pointer anywhere over the applet and click a mouse button. (Click the button again to stop the music.) The figure below reveals the snowfall and lyrics.

Snowflakes fall past a classic Christmas song's lyrics into a snowbank.

As you examine the figure, you will probably want to discover how I created and animated the snowflakes, how I implemented a "glass pane" that dims those portions of snowflakes that fall behind this pane (to achieve a high contrast between snowflakes and lyrics, so that the lyrics are always readable), and how I created the snowbank shown along the bottom of the applet's drawing surface. You'll find out how I accomplished these tasks by studying Listing 1's LetItSnow source code. After this source code, I explain the snowbank creation in detail.

Listing 1. LetItSnow.java

Page 2 of 2
 

// LetItSnow.java

import java.applet.*;

import java.awt.*; import java.awt.event.*; import java.awt.image.*; import java.awt.geom.*;

import java.util.Vector;

/** * This class describes the LetItSnow applet. * * @author Jeff Friesen */

public class LetItSnow extends Applet implements Runnable { /** * Animation loop delay factor (in milliseconds). The paint(Graphics g) * method is called every DELAY milliseconds. */

private final static int DELAY = 80;

/** * Number of SnowFlake objects (and, hence, snowflakes) managed by this * applet. */

private final static int NFLAKES = 50;

/** * Radius of largest snowflake. Diameter is twice this value. */

private final static int MAX_RADIUS = 35;

/** * "Let It Snow! Let It Snow! Let It Snow!" audio clip. */

private AudioClip ac;

/** * Audio clip is playing flag. Not very accurate, but convenient for this * simple applet. */

private boolean fPlaying;

/** * Graphics device on which animation frames are painted. */

private Graphics2D gBuffer;

/** * The graphics device associates with an image buffer. This buffer is * used to eliminate one source of flicker. */

private Image imBuffer;

/** * Applet height. Passed to each SnowFlake object's constructor and also * used to determine image buffer height. */

private int height;

/** * Applet width. Passed to each SnowFlake object's constructor and also * used to determine image buffer width. */

private int width;

/** * "Let It Snow! Let It Snow! Let It Snow!" song lyrics. These lyrics are * displayed on what appears to be a glass pane over the middle of the * applet's drawing surface. Note: I am not certain if these lyrics match * the original lyrics exactly. */

private String [] lyrics = { "Oh, the weather outside is frightful,", "But the fire is so delightful,", "And since we've no place to go,", "Let it snow, let it snow, let it snow.", "It doesn't show signs of stopping,", "And I brought some corn for popping;", "The lights are turned way down low,", "Let it snow, let it snow, let it snow.", "When we finally kiss good night,", "How I'll hate going out in the storm;", "But if you really hold me tight,", "All the way home I'll be warm.", "The fire is slowly dying,", "And, my dear, we're still good-bye-ing,", "But as long as you love me so.", "Let it snow, let it snow, let it snow." };

/** * Animation thread -- created in the start() method and destroyed in the * stop() method. */

private Thread thdAnimate;

/** * Datastructure for managing SnowFlake objects. */

private Vector flakes = new Vector ();

/** * Initialize the applet. SnowFlake objects are created and placed into a * datastructure, a mouse listener is registered for playing and * stopping the music, and an image buffer is created for painting each * animation frame off screen. */

public void init () { width = getWidth ();

height = getHeight ();

for (int i = 0; i < NFLAKES; i++) { int radius = random (MAX_RADIUS-5)+5; int x = random (width-2*radius); int y = -radius*2; int v = radius/5+1; flakes.add (new Snowflake (width, height, radius, x, y, v, random (NFLAKES)+1)); }

ac = getAudioClip (getDocumentBase (), "letitsno.mid");

addMouseListener (new MouseAdapter () { public void mouseClicked (MouseEvent e) { if (fPlaying) ac.stop (); else ac.play ();

fPlaying = !fPlaying; } });

imBuffer = createImage (width, height); gBuffer = (Graphics2D) imBuffer.getGraphics (); gBuffer.setRenderingHint (RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); }

/** * Paint the next animation frame. * * @param g graphics device on which animation frame is painted */

public void paint (Graphics g) { if (gBuffer == null) // Prevent an exception should paint() be called return; // before init() finishes.

// Clear entire frame to black.

gBuffer.setColor (Color.black);

gBuffer.fillRect (0, 0, width, height);

// Paint snowflakes.

for (int i = 0; i < NFLAKES; i++) { Snowflake sf = (Snowflake) flakes.get (i); sf.move (gBuffer); }

// Create and paint snowbank at bottom of frame.

GeneralPath gp = new GeneralPath (); gp.moveTo (0, height-MAX_RADIUS); gp.curveTo (width/4, height-MAX_RADIUS/2, width-width/4, height+MAX_RADIUS/4, width, height-MAX_RADIUS); gp.lineTo (width, height); gp.lineTo (0, height); gp.closePath ();

gBuffer.setPaint (Color.white); gBuffer.fill (gp);

// Create and paint text (with partial transparency) over snowflakes.

gBuffer.setComposite (AlphaComposite.getInstance (AlphaComposite.SRC_OVER, 0.5f));

gBuffer.setPaint (Color.black); Rectangle2D r2d = new Rectangle2D.Double (width/2-130, height/2-157, 260, 315); gBuffer.fill (r2d);

gBuffer.setComposite (AlphaComposite.SrcOver);

gBuffer.setFont (new Font ("Serif", Font.BOLD, 14)); gBuffer.setPaint (Color.yellow);

FontMetrics fm = gBuffer.getFontMetrics (); for (int i = 0; i < lyrics.length; i++) { int swidth = fm.stringWidth (lyrics [i]); gBuffer.drawString (lyrics [i], width/2-130+(260-swidth)/2, height/2-157+fm.getHeight ()*(i+1)); }

// Paint image buffer's animation frame on applet's graphics device.

g.drawImage (imBuffer, 0, 0, this); }

/** * Launch the animation loop. The loop continues as long as the current * thread's reference is the same as the animation thread's reference. * The animation thread's reference is set to null by the applet's stop() * method when the user moves away from the current Webpage -- and hence * the animation stops. */

public void run () { Thread thdCurrent = Thread.currentThread ();

while (thdCurrent == thdAnimate) { repaint ();

try { Thread.sleep (DELAY); } catch (InterruptedException e) { } } }

/** * Start the animation thread, which invokes the run() method. */

public void start () { if (thdAnimate == null) { thdAnimate = new Thread (this); thdAnimate.start (); } }

/** * Stop the animation thread, which causes the run() method to exit. */

public void stop () { thdAnimate = null; }

/** * The AWT invokes the update() method in response to the repaint() method * call that is made in the run() method. The default implementation of * this method, which is inherited from the Container class, clears the * applet's drawing area to the background color prior to calling paint(). * This clearing followed by drawing causes flicker. LetItSnow overrides * update() to prevent the background from being cleared, which eliminates * this source of flicker. * * @param g graphics device on which animation frame is painted */

public void update (Graphics g) { paint (g); }

/** * Obtain a randomly-selected integer between 0 and a limit. * * @param limit one more than the largest integer that can be returned * * @return an integer ranging from 0 through limit-1 */

private int random (int limit) { return (int) (Math.random ()*limit); } }

/** * This class describes a snowflake graphics object. Use Snowflake to create, * and move (in a downwards direction only) snowflakes on a graphics device. * * @author Jeff Friesen */

class Snowflake { /** * The angle between the branch and its two immediate sub-branches, or * between a sub-branch and its two immediate sub-branches. */

private final static double BRANCH_ANGLE = 30.0*Math.PI/180.0;

/** * Multiplier used in determining a branch's/sub-branch's center point. */

private final static double BRANCH_FACTOR = 0.33;

/** * Multiplier used in determining sub-branch length -- each level's two * sub-branches should be smaller than the previous level's sub-branch. */

private final static double SHRINK_FACTOR = 0.66;

/** * Graphics device on which snowflake is painted. */

private Graphics2D gBuffer;

/** * The graphics device associates with an image buffer. For performance * reasons, each snowflake is painted into its own buffer. */

private Image imBuffer;

/** * Applet height. When the snowflake's vertical position exceeds this * height, the snowflake is repositioned to its starting height. */

private int aheight;

/** * Applet width. When the snowflake's vertical position exceeds the * applet height, the snowflake is given a new horizontal position based * on its radius and the applet width. This helps give the illusion of an * unending supply of snowflakes. */

private int awidth;

/** * Center x-coordinate of image buffer, for line-drawing. */

private int centerx;

/** * Center y-coordinate of image buffer, for line-drawing. */ private int centery;

/** * Delay factor used to minimize clumping -- multiple snowflakes that move * together as if they were a single snowflake. This variable must reach * zero before a snowflake can start to move. */

private int pause;

/** * Snowflake radius. A snowflake is considered to be circular, even if it * does not quite appear that way. */

private int radius;

/** * Snowflake's starting horizontal position: 0 through awidth-2*radius-1. */

private int startx;

/** * Snowflake's starting vertical position: this value should be negative, * so that the snowflake can be seen falling into the top of the applet -- * not popping into existence. */

private int starty;

/** * Snowflake velocity. Negative velocities are not considered for this * applet. However, if you want to simulate wind gusts, the velocity could * be negative to illustrate snowflakes being blown upwards. */

private int velocity;

/** * Snowflake's current horizontal position. */

private int x;

/** * Snowflake's current vertical position. */

private int y;

/** * Create a Snowflake object that describes a graphical snowflake. For * performance reasons, the snowflake is predrawn into an image buffer. * * @param awidth applet's drawing surface width * @param aheight applet's drawing surface height * @param radius snowflake's radius * @param startx snowflake's starting x position on drawing surface * @param starty snowflake's starting y position on drawing surface * @param velocity snowflake's velocity * @param pause delay factor used to minimize clumping */

Snowflake (int awidth, int aheight, int radius, int startx, int starty, int velocity, int pause) { this.awidth = awidth; this.aheight = aheight; this.radius = radius; this.startx = startx; this.starty = starty; this.velocity = velocity; this.pause = pause;

centerx = centery = radius; x = startx; y = starty;

imBuffer = new BufferedImage (radius*2, radius*2, BufferedImage.TYPE_INT_ARGB); gBuffer = (Graphics2D) imBuffer.getGraphics (); gBuffer.setRenderingHint (RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

drawSnowflake (gBuffer); }

/** * Move the snowflake downwards according to its velocity. Movement does * not begin until the pause counter reaches zero. (This counter is used * to minimize clumping.) When the snowflake reaches the bottom of the * graphics device's drawing surface, it's recycled back to its starting * vertical position, but with a different horizontal position. * * @param g graphics device on which snowflake is painted */

void move (Graphics2D g) { if (--pause >= 0) return; else pause = 0;

g.drawImage (imBuffer, x, y, null); y += velocity; if (y > aheight) { y = starty; x = (int) (Math.random ()*(awidth-2*radius)); } }

/**

* Draw the snowflake in terms of its six branches, and their three levels * of sub-branches. Each branch is positioned 60 degrees from the previous * branch. Furthermore, each branch is raised 30 degrees above the x axis. * * @param g graphics device on which branch and sub-branches are painted */

private void drawSnowflake (Graphics2D g) { g.setColor (Color.white);

for (int branch = 0; branch < 6; branch++) { double angle = Math.toRadians (branch*60.0+30.0);

drawSnowflakeBranch (g, 0.0, 0.0, rotateX (radius, 0.0, angle), rotateY (radius, 0.0, angle), 0); } }

/** * Draw one of the snowflake's branches and three levels of sub-branches. * * @param g graphics device on which branch and sub-branches are painted * @param startx x component of branch/sub-branch start point * @param starty y component of branch/sub-branch start point * @param endx x component of branch/sub-branch end point * @param endy y component of branch/sub-branch end point * @param depth recursion level (0 represents branch level, 3 represents * third -- and final -- sub-branch level) */

private void drawSnowflakeBranch (Graphics2D g, double startx, double starty, double endx, double endy, int depth) { if (depth == 4) return;

g.setStroke (new BasicStroke (depth > 1 ? 1 : 2)); g.drawLine (centerx+(int) startx, centery+(int) starty, centerx+(int) endx, centery+(int) endy);

double cx = startx+(endx-startx)*BRANCH_FACTOR; double cy = starty+(endy-starty)*BRANCH_FACTOR;

double nendx = cx+(endx-startx)*SHRINK_FACTOR; double nendy = cy+(endy-starty)*SHRINK_FACTOR;

double rx1 = rotateX (nendx-cx, nendy-cy, BRANCH_ANGLE)+cx; double ry1 = rotateY (nendx-cx, nendy-cy, BRANCH_ANGLE)+cy;

double rx2 = rotateX (nendx-cx, nendy-cy, -BRANCH_ANGLE)+cx; double ry2 = rotateY (nendx-cx, nendy-cy, -BRANCH_ANGLE)+cy;

drawSnowflakeBranch (g, cx, cy, rx1, ry1, depth+1); drawSnowflakeBranch (g, cx, cy, rx2, ry2, depth+1); }

/** * Rotate the x component of a point through a clockwise angle around the * (0,0) origin. * * @param x point's horizontal component * @param y point's vertical component * @param angle number of radians in which to rotate x component * * @return equivalent x component following rotation */

private double rotateX (double x, double y, double angle) { return x*Math.cos (angle)+y*Math.sin (angle); }

/** * Rotate the y component of a point through a clockwise angle around the * (0,0) origin. * * @param x point's horizontal component * @param y point's vertical component * @param angle number of radians in which to rotate y component * * @return equivalent y component following rotation */

private double rotateY (double x, double y, double angle) { return -x*Math.sin (angle)+y*Math.cos (angle); } }

Snowbank creation is one of Listing 1's tasks. Although I found it easier to write snowbank-creation code than to write snowflake-creation code, I found it harder to design the snowbank than the snowflakes. My initial attempts, involving combinations of sines and cosines, resulted in hilly snowbanks that repeated in appearance as I increased the applet's width. This repetition caused snowbanks to look cartoonish. What I needed was a curved snowbank spanning the applet's width (no repetition). Java 2D's java.awt.geom.GeneralPath class met this need.

The GeneralPath class lets you construct a shape's border out of straight lines, quadratic curves, and cubic (also known as Bézier) curves. The quadratic curve wasn't quite what I wanted, providing only a single control point to control the shape of the curve. Instead, I chose to use a cubic curve. The public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) method uses two control points—represented by (x1, y1) and (x2, y2)—to describe a cubic curve. Point (x3, y3) describes the curve's final point.

Creating a snowbank shape involved creating a GeneralPath object and calling various methods that describe the snowbank's border in terms of a path (a series of instructions that, when followed, result in the shape's border). The public void moveTo(float x, float y) method sets the initial border position to the coordinates specified by x and y: I chose the left edge of the applet's drawing surface and MAX_RADIUS pixels above the bottom edge of this surface as the initial position. The curveTo() method draws a cubic curve: I chose control points positioned 25 percent from the left edge and 25 percent from the right edge of the drawing surface to describe the curve along the top edge of the shape's border. The public void lineTo(float x, float y) method draws a line from the previous border position (the final curve point, for example) to the position specified by (x, y): I employed this method to draw lines around the right and bottom edges of the drawing surface. Finally, public void closePath() draws a line from the most recent border position to the initial border position: I used that method to ensure the snowbank shape was closed (although this was not necessary, it provides completeness).

After compiling Listing 1, you will want to run this applet. Before you can do that, however, you must describe the applet to appletviewer via HTML. Listing 2 provides the needed HTML.

Listing 2. LetItSnow.html

 <applet code=LetItSnow.class width=450 height=450>
</applet> 

Fix the applet

There is a problem with Listing 1's dependence on the java.applet.AudioClip's public void play() and public void stop() methods to play and stop a song. When the song stops playing (without the applet having been clicked), the fPlaying variable is not set to false. As a result, you must click the applet twice—the first time to invoke stop() (and set fPlaying to false) and the second time to invoke play() (and set fPlaying to true)—to replay the song.

Although the aforementioned problem is trivial, it is somewhat annoying. Unfortunately, AudioClip cannot detect when a playing song ends. However, detection is possible by using Java's Sound API, whose classes and interfaces reside in the javax.sound.midi package. Replace Listing 1's AudioClip code with code based on javax.sound.midi, so you never again need to click the mouse twice to replay the song. Listing 3's PlayMIDI application source code gets you started by showing you how to play a MIDI-based song and detect the song's end.

Listing 3. PlayMIDI.java

 

// PlayMIDI.java

import java.io.*; import java.net.*; import javax.sound.midi.*;

class PlayMIDI { /** * End of track message. */

public final static int END_OF_TRACK = 47;

/** * Program entry point. * * @param args array of command-line arguments * * @throws Exception any old exception */

public static void main (String [] args) throws Exception { if (args.length != 1) { System.err.println ("usage : java PlayMIDI url"); System.err.println ("example: java PlayMIDI file:letitsno.mid"); return; }

playMIDI (new URL (args [0]));

System.exit (0); }

/** * Play a MIDI song. * * @param url the MIDI song's URL * * @throws IOException * @throws InvalidMidiDataException * @throws MidiUnavailableException */

public static void playMIDI (URL url) throws IOException, InvalidMidiDataException, MidiUnavailableException { Sequencer sequencer = null;

try { // Obtain the default sequencer connected to the default synthesizer. // The sequencer plays back the MIDI sequence, sending MIDI messages // or events to the synthesizer. The synthesizer takes note messages // and produces sound.

sequencer = MidiSystem.getSequencer ();

// Open the default sequencer to allocate all necessary resources.

sequencer.open ();

// Specify the URL stream source.

sequencer.setSequence (url.openStream ()); // Register a listener to cause this method to exit when the stream // ends.

sequencer.addMetaEventListener (new MetaEventListener () { public void meta (MetaMessage e) { if (e.getType () == END_OF_TRACK) synchronized ("lock") { "lock".notify (); } } }); // Start MIDI playback.

sequencer.start (); // Block until the listener above notifies the current thread that // it has finished playing the MIDI sequence.

synchronized ("lock") { while (sequencer.isRunning ()) try { "lock".wait (); } catch(InterruptedException e) { } } } finally { if (sequencer != null) sequencer.close (); } } }

The PlayMIDI application requires a single URL argument identifying the MIDI file to be played. For example, java PlayMIDI file:letitsno.mid plays the letitsno.mid MIDI file located in the current directory (on my platform).

Review

To help get you in the mood for the holiday season, this article presented a LetItSnow applet that animates a gentle snowfall while playing "Let It Snow! Let It Snow! Let It Snow!" After explaining how I created the snowbank, I revealed a tiny problem with the applet and showed you a MIDI-playing application's source code to help you get started solving this problem.

Jeff Friesen is a freelance software developer and educator specializing in C, C++, and Java technology.

Learn more about this topic