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.
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.MediaPlayerinstance and have it start playing theresdirectory'slet-it-snow.mp3file via thejavafx.scene.media.Mediaobject, which is responsible for loading this file. -
Start playing all of the translate transitions in the
sftsequence 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:
-
startXspecifies the x component of a branch/sub-branch start point -
startYspecifies the y component of a branch/sub-branch start point -
endXspecifies the x component of a branch/sub-branch end point -
endYspecifies the y component of a branch/sub-branch end point -
depthspecifies 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_ANGLEspecifies the angle between the branch and its two immediate sub-branches, or between a sub-branch and its two immediate sub-branches. -
BRANCH_FACTORspecifies the multiplier used in determining a branch's/sub-branch's center point. -
SHRINK_FACTORspecifies 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!
Note: Application created with JavaFX 1.2 (via NetBeans IDE 6.5.1) on top of Java SE 6u16.









