Wednesday, 10 December 2008

Merry Christmas JavaFX 1.0

JavaFX 1.0 was finally released last Friday. After over 2 years of brooding, I wonder what Sun has come up with - oh boy what a disappointment the JavaFX IDE (NB plug-in) is! But that is for another post.

The JavaFX 1.0 is no good for developing any RIA business applications for its lack of widgets. Instead it is OK for little toys such as those demos published on javafx.com. Immersed in the holiday season atmosphere, I implemented my Christmas card in JavaFX. A screenshot is shown below.

The Christmas card has the following features:

  • Transformation - the red and yellow text fly into the screen using a combination of Translation, Rotation and Scale.
  • Media playback - background music (mp3 file) from Michael Buble - Let it snow.
  • Animation - snow fall simulation
  • Drag out - when run as applet in a browser, the card can be dragged out of the browser.

The Project Structure

The project was created using NetBeans 6.5 with JavaFX plug-ins (installed from within NetBeans). I created a JavaFX project called MerryChristmas. The project structure is shown below:

The only files that I added are:

  1. LetItSnow.mp3 - the background music song
  2. tree.png - the background image: photo of Christmas tree shot at Darling Harbour on December 8, 2008.
  3. Main.fx - the main JavaFX file containing the scene and its contents
  4. SnowFall.fx - the CustomNode simulating snow fall and snow flakes (using circle)

The Main Contents

The contents of the main scene are two Texts, one background image, one background music and the snow fall (implemented as CustomNode).

The texts and their animation (actually transformation) are created as shown below. Notice the transforms which use binding data changed by the Timeline definition.

...
var x:Number;
var y: Number;
var scaleX : Number;
var scaleY:Number;
var angle:Number;

Timeline {
    repeatCount: 1
    keyFrames: [
        at (0s) {x=>180; y => 250; scaleX => 0.0; scaleY => 0.0; angle => -180},
        at (10s) {x=> 0; y => 0;
        scaleX => 1.0; scaleY => 1.0;
        angle => 0
        tween Interpolator.EASEOUT},
    ]
}.play();
...
var lighting = Lighting{
    light: DistantLight{
        azimuth: 60,
        elevation: 70
    }
    surfaceScale: 5
};

Stage {
    ...
    scene: Scene {
        content: [
            ...
            Text {
                content: "Merry Christmas & \nHappy New Year!"
                font: Font.font("Arial Bold", FontWeight.BOLD, 40)
                textOrigin: TextOrigin.TOP
                textAlignment: TextAlignment.CENTER
                x: 10
                y: 20
                transforms: bind[
                    Translate{
                        x:x
                        y:y}
                    Scale{
                        x:scaleX
                        y:scaleY}
                    Rotate{
                        angle: -angle
                        pivotX: 180
                        pivotY: 20 }
                ]
                fill: Color.RED
                effect: lighting
            },
            Text {
                content: "From Romen"
                font: Font.font("Times", FontWeight.BOLD, 40)
                textOrigin: TextOrigin.TOP
                x: 90
                y: 470
                transforms: bind[
                    Translate{
                        x:x
                        y:y}
                    Scale{
                        x:scaleX
                        y:scaleY}
                    Rotate{
                        angle: angle
                        pivotX: 180
                        pivotY: 450}
                ]
                fill: Color.YELLOW
                effect: lighting
            },
...

Any suggestions on how to make unicode text work are welcome.

The background image and music are easy enough:
...
            ImageView {
                image: Image {
                    url: "{__DIR__}tree.png"
                }
                x:0,
                y:0
            },
...
            MediaView {
                mediaPlayer: MediaPlayer {
                    autoPlay: true
                    media: Media {
                        source: "{__DIR__}LetItSnow.mp3"
                    }
                }
            },
...

Drag Out

To enable drag out when run as an applet in browser, I added the following extension to the Stage:
    extensions: [
        AppletStageExtension {
            shouldDragStart: function(e): Boolean {
                return e.primaryButtonDown;
            }
            useDefaultClose: false
        }
    ]
Also I had to specify in the Project's properties dialog and check the Draggable checkbox so that it will generate the .html file with the draggable set to true. The generated .html file snippet:
...

...

Snow Fall

All snow related classes are in the SnowFall.fx file. I used white circle as the snow flake:
public class SnowFlake extends Circle {
    init {
        fill = Color.WHITE;
        radius = 3 + Math.random() * 3;
        opacity= Math.random() * 0.5 + 0.5;
    }
}
The SnowFall class takes 3 attributes:
  • height, width - these specify the size of the canvas so that all the snow flakes can spread out in the canvas making it more realistic
  • numberOfFlakes - the number of snow flakes to be drawn and animated. The more flakes, the more threads JavaFX will have to create and manage under the hood.
The animation of the snow flakes are a bit tricky. Initially all the snow flakes are generated and randomly placed around the canvas; then, they start to fall. The movement of the snowflakes are as such: every second the snow flake will move left (negative) or right (positive) by a random amount, and move down by a random amount - i.e. the horizontal movement (x) can be either positive or negative, the vertical movement (y) is positive only. This is shown below:
                            KeyFrame {
                                ...
                                values: [
                                    x =>
                                    x + (Math.random() - 0.5)
                                    y =>
                                    y + Math.random() tween Interpolator.LINEAR
                                ]
...
When the snow flake hits either side of the boundary, it sort of bounces back a little; when it moves off the bottom of the canvas, it is wrapped around and re-appears at the top of the canvas and falls down again. This is achieved by using action of Keyframe which are executed at the beginning of each key frame.
...
                               action: function() {
                                    if(flake.centerY+y >height) {
                                        y=0; flake.centerY=0;
                                    }
                                    if(flake.centerX+x>width) {
                                        x=0; flake.centerX=width;
                                    }
                                    if(flake.centerX+x<0) {
                                        x=0; flake.centerX=0;
                                    }
                                }
...
The SnowFall class generates all the snow flakes in a for loop. Each snow flake is accompanied by a Timeline controlling its animation. The number of frames in each Timeline is also randomised (between 60 and 120 seconds per cycle), to avoid the situation where all snow flakes suddenly reset at the same time.
for ( i in [1..numberOfFlakes]) {
...
                    def timer = 60+60*Math.random();
                    Timeline {
                        repeatCount: Timeline.INDEFINITE
                        keyFrames: [ for (j in [0..timer]) {
                            KeyFrame {
...
Want to see the Christmas card in action? Well, you will have to wait till Christmas! [Update NYe2008]Now that it is NYE 2008, click here to run the e-card.

Meanwhile, here are the complete source code.

SnowFall.fx

/*
 * SnowFall.fx
 *
 * Created on 10/12/2008, 09:55:19
 */

package merrychristmas;

import java.lang.Math;
import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.animation.KeyFrame;
import java.lang.System;

/**
 * @author ROMENL
 */

public class SnowFall extends CustomNode {
    public var height:Number;
    public var width:Number;
    public var numberOfFlakes:Number;

    override function create():Node {
        return Group {
            content: [
                for ( i in [1..numberOfFlakes]) {
                    var x:Number;
                    var y:Number;
                    x=1;
                    y=1;
                    var flake = SnowFlake{
                        centerX: Math.random() * width
                        centerY: Math.random() * height
                        translateX: bind x;
                        translateY: bind y;
                    }

                    def timer = 60+60*Math.random();
                    Timeline {
                        repeatCount: Timeline.INDEFINITE
                        keyFrames: [ for (j in [0..timer]) {
                            KeyFrame {
                                time: bind Duration.valueOf(j * 1000)
                                values: [
                                    x =>
                                    x + (Math.random() - 0.5)
                                    y =>
                                    y + Math.random() tween Interpolator.LINEAR
                                ]
                                action: function() {
                                    if(flake.centerY+y >height) {
                                        y=0; flake.centerY=0;
                                    }
                                    if(flake.centerX+x>width) {
                                        x=0; flake.centerX=width;
                                    }
                                    if(flake.centerX+x<0) {
                                        x=0; flake.centerX=0;
                                    }
                                }
                            }
                        }
                        ]
                    }.play();
                    flake
                }]
            
        };
    }
}
public class SnowFlake extends Circle {
    init {
        fill = Color.WHITE;
        radius = 3 + Math.random() * 3;
        opacity= Math.random() * 0.5 + 0.5;
    }
}

Main.fx

/*
 * Main.fx
 *
 * Created on 9/12/2008, 15:29:24
 */

package merrychristmas;

import javafx.animation.*;
import javafx.scene.effect.*;
import javafx.scene.effect.light.*;
import javafx.scene.image.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import javafx.scene.paint.Color;
import javafx.scene.Scene;
import javafx.scene.text.*;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate;
import javafx.stage.AppletStageExtension;
import javafx.stage.Stage;

/**
 * @author ROMENL
 */
var width=390;
var height=540;
var x:Number;
var y: Number;
var scaleX : Number;
var scaleY:Number;
var angle:Number;

Timeline {
    repeatCount: 1//Timeline.INDEFINITE
    keyFrames: [
        at (0s) {x=>180; y => 250; scaleX => 0.0; scaleY => 0.0; angle => -180},
        at (10s) {x=> 0; y => 0;
        scaleX => 1.0; scaleY => 1.0;
        angle => 0
        tween Interpolator.EASEOUT},
    ]
}.play();

var lighting = Lighting{
    light: DistantLight{
        azimuth: 60,
        elevation: 70
    }
    surfaceScale: 5
};

Stage {
    title: "Merry Xmas JavaFX"
    width: width
    height: height
    scene: Scene {
        content: [
            ImageView {
                image: Image {
                    url: "{__DIR__}tree.png"
                }
                x:0,
                y:0
            },
            Text {
                content: "Merry Christmas & \nHappy New Year!"
                font: Font.font("Arial Bold", FontWeight.BOLD, 40)
                textOrigin: TextOrigin.TOP
                textAlignment: TextAlignment.CENTER
                x: 10
                y: 20
                transforms: bind[
                    Translate{
                        x:x
                        y:y}
                    Scale{
                        x:scaleX
                        y:scaleY}
                    Rotate{
                        angle: -angle
                        pivotX: 180
                        pivotY: 20 }
                ]
                fill: Color.RED
                effect: lighting
            },
            Text {
                content: "From Romen"
                font: Font.font("Times", FontWeight.BOLD, 40)
                textOrigin: TextOrigin.TOP
                x: 90
                y: 470
                transforms: bind[
                    Translate{
                        x:x
                        y:y}
                    Scale{
                        x:scaleX
                        y:scaleY}
                    Rotate{
                        angle: angle
                        pivotX: 180
                        pivotY: 450}
                ]
                fill: Color.YELLOW
                effect: lighting
            },
            MediaView {
                mediaPlayer: MediaPlayer {
                    autoPlay: true
                    media: Media {
                        source: "{__DIR__}LetItSnow.mp3"
                    }
                }
            },
            SnowFall {
                height: height
                width: width
                numberOfFlakes: 30
            }
        ]
    }
    extensions: [
        AppletStageExtension {
            shouldDragStart: function(e): Boolean {
                return e.primaryButtonDown;
            }
            useDefaultClose: false
        }
    ]
}
Related Posts:
  1. Happy New Year
  2. Happy New Year of the Ox
  3. The Sorry State of JavaFX 1.0

No comments: