Explore Java and more with Jeff 'JavaJeff' Friesen

Happy Holidays

Earlier this month I entered the JFXStudio Holiday Challenge with "Happy Holidays" as my submission. Figure 1 reveals this JavaFX application's scene.

Happy Holidays running on the Mac.

Figure 1: Happy Holidays running on the Mac.

Happy Holidays presents a scene where snowflakes fall into a snowbank while "Let It Snow! Let It Snow! Let It Snow!" (the classic 1945 Christmas song by lyricist Sammy Cahn and composer Jule Styne) plays. When the music ends, Figure 1's logo fades into view.

My submission consisted of Holiday.jar and Main.fx -- I used NetBeans IDE 6.5.1 with JavaFX 1.2 to create Happy Holidays. Main.fx met the challenge's 30/fewer lines or 3000/fewer characters requirement by presenting a single 2996-character line.

To meet the challenge's 3000-character limit, the submitted Main.fx's single line crams all of its code together, leaving only essential spaces. Furthermore, the names of variables and other items are shortened, often hiding their meaning.

Although the submitted Main.fx is hard to understand, the original source code (from a NetBeans HH project) is somewhat easier to grasp. This source code is spread across Main.fx (see Listing 1) and its Snowflake.fx companion.

/*
 * Main.fx
 */

package hh;

import javafx.animation.transition.FadeTransition;
import javafx.animation.transition.TranslateTransition;

import javafx.scene.Group;
import javafx.scene.Scene;

import javafx.scene.effect.Lighting;
import javafx.scene.effect.light.DistantLight;

import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

import javafx.scene.input.MouseEvent;

import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;

import javafx.scene.paint.Color;

import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;

import javafx.scene.text.Text;

import javafx.stage.Stage;

import javafx.util.Math;

def WIDTH = 300;
def HEIGHT = 300;

def NFLAKES = 70;
def MAX_RADIUS = 30;

def snowflakes: Snowflake [] =
    for (i in [0..<NFLAKES])
    {
         var radius = random (MAX_RADIUS-5)+5; // radius range [5,35)
         Snowflake
         {
             radius: radius
             translateX: random (WIDTH-2*radius)
             translateY: -radius*10-random (160)
             color: Color.WHITE
         }
    }

def sft: TranslateTransition [] = 
    for (i in [0..<NFLAKES])
         TranslateTransition
         {
             node: snowflakes [i]
             fromY: snowflakes [i].translateY
             toY: HEIGHT
             duration: Duration.valueOf ((MAX_RADIUS-snowflakes [i].radius)*1500+
                                         1000)
             repeatCount: TranslateTransition.INDEFINITE
             action: function (): Void
             {
                 def radius = snowflakes [i].radius;
                 sft [i].node.translateY = -radius*10-random (160);
                 sft [i].duration = Duration.valueOf ((MAX_RADIUS-radius)*1500+
                                                      1000);
                 snowflakes [i].translateX = random (WIDTH-2*radius)
             }
         }

Stage
{
    title: "Happy Holidays"

    resizable: false

    var scene: Scene;
    scene: scene = Scene
    {
        width: WIDTH
        height: HEIGHT

        fill: Color.BLACK

        content: Group
        {
            var ivBG: ImageView
            var ivLogo: ImageView
            var text: Text
            content:
            [
                ivBG = ImageView
                {
                    image: Image
                    {
                        url: "{__DIR__}res/bg.jpg"
                    }

                    fitWidth: WIDTH
                    fitHeight: HEIGHT
                    preserveRatio: false

                    onMouseClicked: function (me: MouseEvent): Void
                    {
                        ivBG.disable = true;
                        text.opacity = 0.0;
                        
                        MediaPlayer
                        {
                            media: Media
                            {
                                source: "{__DIR__}res/let-it-snow.mp3"
                            }
                            onEndOfMedia: function (): Void
                            {
                                FadeTransition
                                {
                                    node: ivLogo
                                    fromValue: 0.0
                                    toValue: 0.85
                                    duration: 5.0s
                                }.play ()
                            }
                        }.play ();

                        for (i in [0..<sizeof sft])
                             sft [i].play ()
                    }
                }
                
                Group 
                { 
                    content: snowflakes,
                    effect: Lighting
                    {
                        diffuseConstant: 0.6
                        specularConstant: 1.7
                        specularExponent: 5.0
                        surfaceScale: 2.0
                        light: DistantLight
                        {
                            azimuth: 0.0
                            elevation: 31.0
                            color: Color.WHITE
                        }
                    }
                }

                Path
                {
                    elements:
                    [
                        MoveTo
                        {
                            x: 0
                            y: HEIGHT-MAX_RADIUS
                        }
                        CubicCurveTo
                        {
                            controlX1: WIDTH/4
                            controlY1: HEIGHT-MAX_RADIUS/.5
                            controlX2: WIDTH-WIDTH/4
                            controlY2: HEIGHT+MAX_RADIUS/4
                            x: WIDTH
                            y: HEIGHT-MAX_RADIUS
                        }
                        LineTo
                        {
                            x: WIDTH
                            y: HEIGHT
                        }
                        LineTo
                        {
                            x: 0
                            y: HEIGHT
                        }
                    ]

                    stroke: Color.WHITE
                    fill: Color.WHITE
                }

                ivLogo = ImageView
                {
                    image: Image
                    {
                        url: "{__DIR__}res/logo.gif"
                    }
                    opacity: 0.0
                    translateX: bind (scene.width-ivLogo.layoutBounds.width)/2
                    translateY: bind (scene.height-ivLogo.layoutBounds.height)/2
                }

                text = Text
                {
                    content: "Click to start"
                    translateX: bind (scene.width-text.layoutBounds.width)/2
                    translateY: bind (scene.height-text.layoutBounds.height)/2
                    fill: Color.GOLD
                }
            ]
        }
    }
}

function random (limit: Number): Number
{
    Math.random ()*limit
}

Listing 1: Main.fx

Main.fx first creates NFLAKES Snowflake custom node objects and stores them in a snowflakes sequence. In the interest of presenting snowflakes in different sizes, each node is assigned a randomly-generated radius, which lies between 5 and 35 pixels.

Each node is also given an initial position by assigning randomly-generated values to its translateX and translateY variables. The former value is chosen so that the snowflake is fully visible horizontally; the latter value positions the node above the stage's scene.

I originally planned to have each snowflake's radius solely determine its initial height by assigning -10*radius to translateY -- the larger the radius, the higher the height. To make the snowfall more interesting, I subsequently offset the height with a random value (between 0 and 160).

Perhaps you're curious about the color: Color.WHITE assignment -- aren't snowflakes always white? Although they're always white in this version of the program, I initially planned to color snowflakes white and various shades of gray to make the snowfall more realistic.

Specifically, I planned to render larger (closer) snowflakes in front of smaller (more distant) snowflakes, and color smaller snowflakes in darker shades of gray (based on radius). However, this extra code would have made it harder to meet the 3000-character limit.

Main.fx next creates an sft sequence of javafx.animation.transition.TranslateTransition instances, one instance per snowflake node. Each transition object is responsible for making its node appear to move down the scene in a certain time period.

The transition translates the node from its initial height to the scene's height (by repeatedly adjusting translateY) in duration milliseconds. Duration is based on the inverse of snowflake radius: the smaller the radius, the longer the snowflake takes to move down the scene.

The minimum duration value should be 1000 milliseconds. Unfortunately, because of a bug in the code (MAX_RADIUS isn't the largest radius; MAX_RADIUS+5 is the largest radius), it's possible to achieve a negative duration value.

Although the transition runs indefinitely, each transition cycle ends when the node's translateY value reaches the scene's height. At this point, the transition's action() function executes, resetting the snowflake's position -- resetting the duration is an unnecessary oversight.

Main.fx finally establishes the stage's scene, which consists of a starry/foggy background image, snowflakes in front of this background, and a snowbank in front of the snowflakes (so that they appear to fall into the snowbank). An image-based logo and text are also part of the scene.

I originally established a black background for the scene via the fill: Color.BLACK assignment in the Scene object literal, which I forgot to remove when I subsequently chose to present an image as the scene's background, to make the scene more realistic.

The background image (in the res directory's bg.jpg file) is larger than the scene's dimensions. To fit these dimensions, I assigned the scene's width and height to the background image's javafx.scene.image.ImageView object's fitWidth and fitHeight variables.

The animation doesn't begin until the user clicks the mouse over the background image. In response to a click, the function assigned to the ImageView instance's onMouseClicked variable starts to execute, accomplishing four tasks:

  • Disable the background image node so that it cannot be clicked on again. I do this to prevent the snowfall animation and music from being disturbed. Also, it leads to shorter code (and I want to keep the final code length to within 3000 characters).
  • Set the opacity of the scene's text node (which informs the user that they must click the scene to start the animation and music) to zero, which causes this node (which appears on top of everything else) to disappear before the animation starts.
  • Create a javafx.scene.media.MediaPlayer instance and have it start playing the res directory's let-it-snow.mp3 file via the javafx.scene.media.Media object, which is responsible for loading this file.
  • Start playing all of the translate transitions in the sft sequence to establish the snowfall animation. As previously mentioned, these transitions run indefinitely -- the snowfall continues until you end the application.

When the music stops (although the snowfall continues), the function assigned to MediaPlayer's onEndOfMedia variable starts to execute. It instantiates and plays a javafx.animation.transition.FadeTransition instance to fade the "Happy Holidays" logo into view.

The logo fades into view over a five-second period. The transition continually adjusts the logo node's opacity variable from its initial invisible value (0.0) to a value (0.85) that's somewhat less than opaque, so that snowflakes can be seen falling behind the logo.

Additional items of interest include wrapping the snowflakes sequence in a javafx.scene.Group node so that a 3D lighting effect can be assigned to all snowflake nodes, and the use of javafx.scene.shape.Path and companion classes to efficiently create the snowbank node.

That's it for Main.fx so let's turn our attention to Snowflake.fx. This file (see Listing 2) presents the source code to the Snowflake class, which Listing 1 references, and which I merged into the Main.fx file that I submitted to the JFXStudio Holiday Challenge.

/*
 * Snowflake.fx
 */

package hh;

import javafx.scene.CustomNode;
import javafx.scene.Node;

import javafx.scene.paint.Color;

import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;

import javafx.util.Math;

public class Snowflake extends CustomNode
{
    public-init var radius: Number;
    public-init var color: Color;

    def BRANCH_ANGLE = 30.0*Math.PI/180.0;
    def BRANCH_FACTOR = 0.33;
    def SHRINK_FACTOR = 0.66;
    
    var pathElements: PathElement [];

    override function create (): Node
    {
        Path
        {
            elements: for (branch in [0..5])
            {
                var angle = Math.toRadians (branch*60.0+30.0);
                delete pathElements;
                snowFlakeBranch (0.0, 0.0, rotateX (radius, 0.0, angle),
                                 rotateY (radius, 0.0, angle), 0);
                pathElements
            }
            stroke: color
        }
    }

    function snowFlakeBranch (startX: Number, startY: Number,
                              endX: Number, endY: Number, depth: Integer): Void
    {
        if (depth == 4)
            return;

        insert MoveTo
        {
            x: startX
            y: startY
        }
        into pathElements;

        insert LineTo
        {
            x: endX
            y: endY
        }
        into pathElements;

        def cX = startX+(endX-startX)*BRANCH_FACTOR;
        def cY = startY+(endY-startY)*BRANCH_FACTOR;
        def nendX = cX+(endX-startX)*SHRINK_FACTOR;
        def nendY = cY+(endY-startY)*SHRINK_FACTOR;
        def rX1 = rotateX (nendX-cX, nendY-cY, BRANCH_ANGLE)+cX;
        def rY1 = rotateY (nendX-cX, nendY-cY, BRANCH_ANGLE)+cY;
        def rX2 = rotateX (nendX-cX, nendY-cY, -BRANCH_ANGLE)+cX;
        def rY2 = rotateY (nendX-cX, nendY-cY, -BRANCH_ANGLE)+cY;

        snowFlakeBranch (cX, cY, rX1, rY1, depth+1);
        snowFlakeBranch (cX, cY, rX2, rY2, depth+1)
    }

    function rotateX (x: Number, y: Number, angle: Number): Number
    {
        x*Math.cos (angle)+y*Math.sin (angle)
    }

    function rotateY (x: Number, y: Number, angle: Number): Number
    {
       -x*Math.sin (angle)+y*Math.cos (angle)
    }
}

Listing 2: Snowflake.fx

Snowflake.fx's Snowflake class introduces a snowflake node type by extending javafx.scene.CustomNode and overriding this class's create(): Node function, which returns a snowflake node as a path-based shape.

The shape is built in terms of six branches, where each branch is positioned 60 degrees from the previous branch, and is raised 30 degrees above the x axis. For each of the six branches, its shape geometry is stored in pathElements, which is added to Path's elements variable.

create() relies upon the snowFlakeBranch() helper function to create a branch's geometry. Prior to invoking snowFlakeBranch(), create() deletes the previous branch geometry from pathElements, which would impact the current branch's geometry if not deleted.

snowFlakeBranch() recursively creates a branch in terms of sub-branches, and relies on five parameters to accomplish this task:

  • startX specifies the x component of a branch/sub-branch start point
  • startY specifies the y component of a branch/sub-branch start point
  • endX specifies the x component of a branch/sub-branch end point
  • endY specifies the y component of a branch/sub-branch end point
  • depth specifies the recursion level (0 represents branch level, 3 represents third and final sub-branch level)

snowFlakeBranch() also relies on rotateX() and rotateY() functions to help in positioning sub-branches by rotating a point through a clockwise angle around the (0, 0) origin, and on the following constants:

  • BRANCH_ANGLE specifies the angle between the branch and its two immediate sub-branches, or between a sub-branch and its two immediate sub-branches.
  • BRANCH_FACTOR specifies the multiplier used in determining a branch's/sub-branch's center point.
  • SHRINK_FACTOR specifies the multiplier used in determining sub-branch length -- each level's two sub-branches should be smaller than the previous level's sub-branch.

Conclusion

Now that you've explored Happy Holidays, you might want to improve this application by adding a more realistic snowfall (as discussed earlier), displaying a glowing moon in front of the background, scrolling the song's lyrics across the screen while it plays, and so on. Have fun!


Download code.zip

Note: Application created with JavaFX 1.2 (via NetBeans IDE 6.5.1) on top of Java SE 6u16.


LEARN JAVA FROM APRESS