Newsletter sign-up
View all newsletters

Enterprise Java Newsletter
Stay up to date on the latest tutorials and Java community news posted on JavaWorld

JavaWorld Daily Brew

Java Tutor

Java Tutor is my platform for teaching about Java 7+ and JavaFX 2.0+, mainly via programming projects.


Rebooting JavaFX, Part 2

 

In the first part, of this two-part tutorial series on JavaFX 2.0, I showed you how to install the JavaFX 2.0.2 SDK, presented JavaFX's architecture, and revealed a rich "Hello, World"-style application that introduced you to JavaFX application architecture and JavaFX APIs.

Part 2 continues to explore JavaFX 2.0, but does so in the context of a Slideshow application. It first introduces you to this application's JavaFX Script source code, and then shows you how to migrate this source code to Java.

You next learn how to improve the Java version of the Slideshow application by adding text-based captions and music. Lastly, you encounter a new JavaFX effect called Wave, explore Wave's architecture, and apply this new effect to Slideshow.

A Slideshow of Solar System Images

I previously created a small JavaFX Script-based Slideshow application for an article on JavaFX application deployment. This application presents a slideshow of solar system images. For example, Figure 1 shows a transition from Jupiter to a closeup of this planet and its moon Io.

Figure 1: A ghostly Jupiter. (Click to enlarge.)

Listing 1 presents Slideshow's cleaned up and slightly improved source code.

// Slideshow.fx

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;

import javafx.animation.transition.FadeTransition;

import javafx.scene.Scene;

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

import javafx.scene.paint.Color;

import javafx.stage.Stage;

class Model
{
   var ivScene: ImageView[];

   function play(): Void
   {
      timeline.play()
   }

   function stop(): Void
   {
      timeline.stop()
   }

   var ivArray: ImageView[];

   init
   {
      def names = [ "sol", "mercury", "mercsurf", "venus", "vensurf",
                    "earthsys", "earth", "iss", "moon", "earthrise",
                    "marssys", "mars", "marsurf1", "marsurf2", "marsurf3",
                    "marsurf4", "phobos", "deimos", "emmars", "gaspra", "ida",
                    "cv", "jupsys", "jupiter", "jupclose", "metis",
                    "adrastea", "amalthea", "thebe", "io", "europa",
                    "ganymede", "callisto", "satsys", "saturn", "pan", "atlas",
                    "prometheus", "pandora", "epimetheus", "janus", "mimas",
                    "enceladus", "tethys", "telesto", "calypso", "dione",
                    "helene", "rhea", "titan", "titsurf", "hyperion",
                    "iapetus", "phoebe", "urasys", "uranus", "puck", "miranda",
                    "ariel", "umbriel", "titania", "oberon", "nepsys",
                    "neptune", "naiad", "thalassa", "despina", "galatea",
                    "larissa", "proteus", "triton", "nereid", "quaoar",
                    "plutosys", "pluto", "makemake", "eris", "sedna", "kb",
                    "halley", "halnuc", "oort", "edge" ];
      for (name in names)
         insert ImageView
         {
            var imageRef: Image
            image: imageRef = Image { url: "{__DIR__}{name}.jpg" }
         }
         into ivArray;

      ivScene = [ ivArray[1], ivArray[0] ]
   }

   var i = 2;

   def fadeout = FadeTransition
   {
      node: bind ivScene[1]
      fromValue: 1.0
      toValue: 0.0
      duration: 2s
      action: function(): Void
      {
         ivScene = reverse ivScene;
         ivScene[0] = ivArray[i];
         ivScene[0].opacity = 1.0;
         if (++i == sizeof ivArray)
            i = 0;
      }
   }

   def timeline: Timeline = Timeline
   {
      repeatCount: Timeline.INDEFINITE
      keyFrames: KeyFrame
      {
         time: 4s
         action: function(): Void
         {
            fadeout.playFromStart();
         }
      }
   }
}

Stage
{
   title: "Solar System Slideshow"

   width: 510
   height: 430
   resizable: false

   scene: Scene
   {
      content: bind model.ivScene

      fill: Color.BLACK
   }

   onClose: function(): Void
   {
      model.stop()
   }
}

var model = Model {}
model.play();

Listing 1: Slideshow.fx

Listing 1 describes an application that repeatedly fades out the top image so that the bottom image can show through, moves the bottom image to the top, and creates a new bottom image. This source code is architected as follows:

  • Various import statements are specified to import JavaFX API types.
  • A Model class is declared to store the application's model, which consists of slide images and the functionality for performing a slideshow of these images. It's a good idea to separate model data from the user interface to simplify application architecture. Model consists of the following entities:

    • ivScene stores a sequence (array-like construct) of ImageView nodes. Exactly two ImageView nodes are stored in this sequence. The first node (at index 0) represents the bottom image and the second node (at index 1) represents the top image (the one that's initially displayed).
    • play() and stop() are Model functions for starting and stopping the slideshow. Each function delegates to a Timeline function on the timeline object to accomplish its task.
    • ivArray stores a sequence of ImageView nodes. Unlike ivScene, ivArray stores one ImageView node for each image that comprises the slideshow.
    • init is the JavaFX Script equivalent of a Java constructor for performing initialization tasks. This construct is used to populate ivArray from the sequence of slideshow image names, as follows: For each image name, instantiate ImageView, load the image with the help of the Image class, assign the Image instance that wraps the loaded image to the ImageView instance's image property, and use JavaFX Script's insert operator to insert the ImageView object into ivArray -- the inserted image view node is appended to the end of the sequence.
    • i is a numeric variable that indexes into ivArray. This variable is used to select the next image to appear as the bottom image. Because the slideshow is initialized with the first two ivArray images, i is initialized to 2 so that it can start with the third image in the sequence.
    • fadeout is a constant that's initialized to a FadeTransition instance, which is a precanned animation for fading out or fading in an image by changing its ImageView node's opacity. This instance's node property is bound to ivScene[1] (the top image) whose opacity will be animated from opaque to transparent so that the bottom image shows completely. Binding ensures that, when ivScene[1] is assigned a new ImageView instance, that instance will have its opacity animated. The fromValue and toValue properties are assigned 1.0 and 0.0 (respectively) to ensure that animation moves from opaque to transparent. The duration property is assigned 2s to ensure that animation takes two seconds to complete. Finally, the action property is assigned a function that gets executed when the animation finishes. This function first reverses the iScene sequence by using the reverse operator to swap the two ImageView instances, then replaces the bottom image (ivScene[0]) with ivArray[i] so that the next fade animation will result in the new bottom image appearing, sets this ImageView node's opacity to be fully opaque (its opacity is transparent at the end of an animation), and advances i to index the next ImageView node that will serve as the bottom node in the scene.
    • timeline is a constant that's initialized to a Timeline instance, which describes a timeline on which an animation cycle plays out. This instance's repeatCount property is assigned Timeline.INDEFINITE so that a new animation cycle will begin automatically after the current animation cycle, and that this pattern will continue until this timeline is stopped (via timeline.stop()). ThekeyFrames property is assigned a single KeyFrame instance that identifies a significant event in the life of the animation. In this case, the event indicates the end of the animation cycle. This cycle ends after four seconds, which is indicated by assigning 4s to this instance's time property. After this much time has passed, the anonymous function assigned to this instance's action property is called and executes fadeout.playFromStart() to launch a new fadeout animation cycle.
  • The Stage class is instantiated to present the application's main window and display the model's scene within this window. (One feature of JavaFX Script is that you can mix class declarations with standalone imperative code in the same source file.) This window's title is set to Solar System Slideshow by assigning this value to the Stage instance's title property. The values assigned to Stage's width and height properties identify the window's dimensions (these dimensions include the titlebar and window border area). The resizable property is assigned false so that the window cannot be resized. The scene property is assigned a Scene instance that describes the scene: its content property is bound to the model's ivScene array so that the scene always displays an animation between the current two images, and its fill property is assigned Color.BlACK so that the scene's background color is black. Finally, Stage's onClose property is assigned an anonymous function that stops the animation when the user closes the main window.
  • Model is instantiated and told to start playing the slideshow by calling its play() function.

Migrating Slideshow's JavaFX Script Source Code to Java

Now that you have a basic understanding of Slideshow's JavaFX Script code, you're ready to explore a JavaFX 2.0 approximation. Listing 2 presents this version's Java source code.

// Slideshow.java

import javafx.animation.FadeTransition;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;

import javafx.application.Application;

import javafx.event.ActionEvent;
import javafx.event.EventHandler;

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

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

import javafx.scene.paint.Color;

import javafx.stage.Stage;

import javafx.util.Duration;

class Model
{
   ImageView[] ivScene = new ImageView[2];

   void play()
   {
      timeline.play();
   }

   void stop()
   {
      timeline.stop();
   }

   Model(final Group root)
   {
      String[] names = new String[]
      {
         "sol", "mercury", "mercsurf", "venus", "vensurf", "earthsys", "earth",
         "iss", "moon", "earthrise", "marssys", "mars", "marsurf1", "marsurf2",
         "marsurf3", "marsurf4", "phobos", "deimos", "emmars", "gaspra", "ida",
         "cv", "jupsys", "jupiter", "jupclose", "metis", "adrastea",
         "amalthea", "thebe", "io", "europa", "ganymede", "callisto", "satsys",
         "saturn", "pan", "atlas", "prometheus", "pandora", "epimetheus",
         "janus", "mimas", "enceladus", "tethys", "telesto", "calypso",
         "dione", "helene", "rhea", "titan", "titsurf", "hyperion", "iapetus",
         "phoebe", "urasys", "uranus", "puck", "miranda", "ariel", "umbriel",
         "titania", "oberon", "nepsys", "neptune", "naiad", "thalassa",
         "despina", "galatea", "larissa", "proteus", "triton", "nereid",
         "quaoar", "plutosys", "pluto", "makemake", "eris", "sedna", "kb",
         "halley", "halnuc", "oort", "edge"
      };

      final ImageView[] ivArray = new ImageView[names.length];
      for (int i = 0; i < names.length; i++)
         ivArray[i] = new ImageView(new Image(names[i]+".jpg"));

      ivScene[0] = ivArray[1];
      ivScene[1] = ivArray[0];

      final FadeTransition fadeout = new FadeTransition();
      fadeout.setNode(ivScene[1]);
      fadeout.setFromValue(1.0);
      fadeout.setToValue(0.0);
      fadeout.setDuration(Duration.valueOf("2000ms"));
      EventHandler<ActionEvent> eh;
      eh = new EventHandler<ActionEvent>()
      {
         public void handle(ActionEvent ae)
         {
            ImageView ivTemp = ivScene[0];
            ivScene[0] = ivScene[1];
            ivScene[1] = ivTemp;
            fadeout.setNode(ivScene[1]);
            ivScene[0] = ivArray[i];
            ivScene[0].setOpacity(1.0);
            if (++i == ivArray.length)
               i = 0;
            root.getChildren().setAll(ivScene[0], ivScene[1]);
         }
      };
      fadeout.setOnFinished(eh);

      timeline = new Timeline();
      timeline.setCycleCount(Timeline.INDEFINITE);
      KeyFrame[] kf = new KeyFrame[1];
      eh = new EventHandler<ActionEvent>()
      {
         public void handle(ActionEvent ae)
         {
            fadeout.playFromStart();
         }
      };
      kf[0] = new KeyFrame(Duration.valueOf("4000ms"), eh);
      timeline.getKeyFrames().addAll(kf);
   }

   private int i = 2;
   private Timeline timeline;
}

public class Slideshow extends Application
{
   public static void main(String[] args)
   {
      Application.launch(args);
   }

   private Model model;

   @Override
   public void start(Stage primaryStage)
   {
      primaryStage.setTitle("Solar System Slideshow");
      primaryStage.setWidth(510);
      primaryStage.setHeight(430);
      primaryStage.setResizable(false);

      Group root = new Group();
      Scene scene = new Scene(root, Color.BLACK);
      model = new Model(root);
      root.getChildren().setAll(model.ivScene[0], model.ivScene[1]);
      model.play();

      primaryStage.setScene(scene);
      primaryStage.show();
   }

   @Override
   public void stop()
   {
      model.stop();
   }
}

Listing 2: Slideshow.java (version 1)

For convenience, Listing 2 presents its equivalent Java source code in roughly the same order as Listing 1. Rather than revisit portions of the source code that were discussed in the first part of this series, I'll focus only on the differences in the following discussion.

The Model class first declares a javafx.scene.image.ImageView array named ivScene. This two-element array corresponds to the ivScene sequence in Listing 1. Model next declares equivalent play() and stop() methods.

Model's Model(final Group root) constructor is the equivalent of JavaFX Script's init construct. The javafx.scene.Group argument passed to root is updated as ImageView nodes are changed during the slideshow.

After declaring an array of image names, the constructor creates and populates ivArray. Each ImageView entry is based on an instance of the javafx.scene.image.Image class that encapsulates the filename of an image.

At this point, the constructor instantiates the javafx.scene.animation.FadeTransition class (which fades one node into another by adjusting its opacity) and initializes this instance by calling various FadeTransition methods:

  • void setNode(Node value) sets the target node of the transition (the node whose opacity is being changed).
  • void setFromValue(double value) sets the node's opacity property's initial value to value (which ranges from 0.0 [transparent] to 1.0 [opaque]).
  • void setToValue(double value) sets the node's opacity property's final value to value.
  • void setDuration(Duration value) sets the animation's duration to value, a javafx.util.Duration instance that represents a time value (in milliseconds).
  • void setOnFinished(EventHandler<ActionEvent> value) installs a javafx.event.EventHandler<T extends Event> instance whose void handle(T event) method is invoked (where T is replaced with ActionEvent) when the animation finishes. FadeTransition inherits setOnFinished() from its abstract javafx.animation.Animation class ancestor. EventHandler is an interface.

    The event handler responds to the action event by swapping ivScene[0] and ivScene[1], calling FadeTransition's setNode() method to notify this object that ivScene[1] contains the new top image ImageView node, assigns the next bottom image's ImageView node to ivScene[0], restores ivScene[0]'s opacity to 1.0 (opaque) because the animation sets the ImageView node's opacity property to 0.0 (transparent) and we want this opacity to be opaque when the slideshow begins a new cycle (otherwise all we'll see is a black background), increments i so that a new bottom image node will be assigned to ivScene[0] when the event handler is subsequently called, and notifies the group that the nodes have changed (so that they'll be displayed) by executing root.getChildren().setAll(ivScene[0], ivScene[1]);.

The constructor closes by creating and initializing a timeline with a single keyframe. Once again, an event handler is used to receive an action event that is fired when the animation cycle ends.

The Slideshow class is pretty similar to Part 1's HelloJavaFX class. The key differences between these classes' start() methods are as follows:

  • The javafx.stage.Stage class's void setResizable(boolean value) method is called with a false argument to prevent the stage window from being resized.
  • The Model class is instantiated.
  • The root.getChildren().add(text); expression is replaced with root.getChildren().setAll(model.ivScene[0], model.ivScene[1]);, which clears out the root group's previous nodes and installs the specified nodes as the group's new nodes. In this case, we want to see the first image and observe the animation slowly replace it with the image immediately below. This expression is also present in Model's constructor, where it is used to ensure that the previous bottom image becomes the top image to be faded out and replaced by the new bottom image.
  • The Model class instance's play() method is called to start the slideshow.

Also, Slideshow overrides Application's void stop() method to stop the slideshow by invoking model.stop(). This method is called when the user closes the stage window.

Listing 2 presents the same slideshow as Listing 1. To see this for yourself, make sure that both source files are located in the same directory as the images, and execute the following command sequences:

javafxc Slideshow.fx
javafx Slideshow

javac -cp "c:\progra~1\oracle\javafx 2.0 runtime\lib\jfxrt.jar" Slideshow.java
java -cp "c:\progra~1\oracle\javafx 2.0 runtime\lib\jfxrt.jar";. Slideshow

The first command sequence assumes that you've installed JavaFX 1.3.1 (Build 01), and the second command sequence assumes that you've installed the JavaFX 2.0.2 runtime to c:\progra~1\oracle\javafx 2.0 runtime.

Binding an ImageView Node to the Fade Transition

Although Listing 2's application works perfectly, it doesn't properly translate Listing 1's node: bind ivScene[1] assignment. Instead, it specifies a pair of fadeout.setNode(ivScene[1]) expressions to keep the slideshow fading toward the newest bottom image.

We should take advantage of binding in case Slideshow is updated at a later time and the update accesses ivScene[1] from another scene location. Forgetting to include code similar to fadeout.setNode(ivScene[1]); in the event handler would constitute a bug.

Listing 3 presents a second version of Slideshow that takes advantage of JavaFX 2.0 binding to translate node: bind ivScene[1] into equivalent JavaFX 2.0 code. The differences between both Slideshow versions are highlighted in red.

// Slideshow.java

import javafx.animation.FadeTransition;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;

import javafx.application.Application;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;

import javafx.event.ActionEvent;
import javafx.event.EventHandler;

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

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

import javafx.scene.paint.Color;

import javafx.stage.Stage;

import javafx.util.Duration;

class Model
{
   ImageView ivScene0;
   ObjectProperty<ImageView> ivScene1 = new SimpleObjectProperty<ImageView>();

   void play()
   {
      timeline.play();
   }

   void stop()
   {
      timeline.stop();
   }

   Model(final Group root)
   {
      String[] names = new String[]
      {
         "sol", "mercury", "mercsurf", "venus", "vensurf", "earthsys", "earth",
         "iss", "moon", "earthrise", "marssys", "mars", "marsurf1", "marsurf2",
         "marsurf3", "marsurf4", "phobos", "deimos", "emmars", "gaspra", "ida",
         "cv", "jupsys", "jupiter", "jupclose", "metis", "adrastea",
         "amalthea", "thebe", "io", "europa", "ganymede", "callisto", "satsys",
         "saturn", "pan", "atlas", "prometheus", "pandora", "epimetheus",
         "janus", "mimas", "enceladus", "tethys", "telesto", "calypso",
         "dione", "helene", "rhea", "titan", "titsurf", "hyperion", "iapetus",
         "phoebe", "urasys", "uranus", "puck", "miranda", "ariel", "umbriel",
         "titania", "oberon", "nepsys", "neptune", "naiad", "thalassa",
         "despina", "galatea", "larissa", "proteus", "triton", "nereid",
         "quaoar", "plutosys", "pluto", "makemake", "eris", "sedna", "kb",
         "halley", "halnuc", "oort", "edge"
      };

      final ImageView[] ivArray = new ImageView[names.length];
      for (int i = 0; i < names.length; i++)
         ivArray[i] = new ImageView(new Image(names[i]+".jpg"));

      ivScene0 = ivArray[1];
      ivScene1.set(ivArray[0]);

      final FadeTransition fadeout = new FadeTransition();
      fadeout.nodeProperty().bind(ivScene1);
      fadeout.setFromValue(1.0);
      fadeout.setToValue(0.0);
      fadeout.setDuration(Duration.valueOf("2000ms"));
      EventHandler<ActionEvent> eh;
      eh = new EventHandler<ActionEvent>()
      {
         public void handle(ActionEvent ae)
         {
            ImageView ivTemp = ivScene0;
            ivScene0 = ivScene1.get();
            ivScene1.set(ivTemp);
            ivScene0 = ivArray[i];
            ivScene0.setOpacity(1.0);
            if (++i == ivArray.length)
               i = 0;
            root.getChildren().setAll(ivScene0, ivScene1.get());
         }
      };
      fadeout.setOnFinished(eh);

      timeline = new Timeline();
      timeline.setCycleCount(Timeline.INDEFINITE);
      KeyFrame[] kf = new KeyFrame[1];
      eh = new EventHandler<ActionEvent>()
      {
         public void handle(ActionEvent ae)
         {
            fadeout.playFromStart();
         }
      };
      kf[0] = new KeyFrame(Duration.valueOf("4000ms"), eh);
      timeline.getKeyFrames().addAll(kf);
   }

   private int i = 2;
   private Timeline timeline;
}

public class Slideshow extends Application
{
   public static void main(String[] args)
   {
      Application.launch(args);
   }

   private Model model;

   @Override
   public void start(Stage primaryStage)
   {
      primaryStage.setTitle("Solar System Slideshow");
      primaryStage.setWidth(510);
      primaryStage.setHeight(430);
      primaryStage.setResizable(false);

      Group root = new Group();
      Scene scene = new Scene(root, Color.BLACK);
      model = new Model(root);
      root.getChildren().setAll(model.ivScene0, model.ivScene1.get());
      model.play();

      primaryStage.setScene(scene);
      primaryStage.show();
   }

   @Override
   public void stop()
   {
      model.stop();
   }
}

Listing 3: Slideshow.java (version 2)

JavaFX 2.0 binding works with properties that can be observed for changes. A property value is encapsulated in an instance of an appropriate property class. When the value changes, the property is updated.

It's important to immediately update fadeout's node property whenever a new ImageView node (which contains the next slideshow image) is assigned to ivScene1. This task is accomplished with help from the javafx.beans.property.ObjectProperty class.

An ivScene1 variable of type ObjectProperty is introduced and assigned a SimpleObjectProperty instance, which can only wrap itself around ImageView instances. The ivScene0 variable remains of type ImageView because it doesn't need to be observed.

Listing 2's ivScene[1] = ivArray[0]; assignment is replaced with ivScene1.set(ivArray[0]);, which uses ObjectProperty's set() method to wrap this object around the first ImageView instance in ivArray (the initial top image).

fadeout.nodeProperty().bind(ivScene1); is used to bind the fadeout object's node property to ivScene1. When a new ImageView object is stored in ivScene1, this property will automatically update to refer to the ImageView object.

The ivScene1.set(ivTemp) expression in fadeout's event handler's handle() method stores the previous bottom image's ImageView node as the new top image ImageView node in SimpleObjectProperty. Binding makes this node available to fadeout immediately.

Binding Model Nodes to the Scene

Listing 3 nicely translates Listing 1's node: bind ivScene[1] expression, but doesn't do anything with Listing 1's content: bind model.ivScene binding expression. However, Listing 4 addresses this limitation. Changes between Listings 3 and 4 are highlighted in red.

// Slideshow.java

import javafx.animation.FadeTransition;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;

import javafx.application.Application;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;

import javafx.event.ActionEvent;
import javafx.event.EventHandler;

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

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

import javafx.scene.paint.Color;

import javafx.stage.Stage;

import javafx.util.Duration;

class Model
{
   private ObjectProperty<ImageView> ivScene0 = 
      new SimpleObjectProperty<ImageView>();
   private ObjectProperty<ImageView> ivScene1 = 
      new SimpleObjectProperty<ImageView>();

   void play()
   {
      timeline.play();
   }

   void stop()
   {
      timeline.stop();
   }

   Model(final Group root)
   {
      String[] names = new String[]
      {
         "sol", "mercury", "mercsurf", "venus", "vensurf", "earthsys", "earth",
         "iss", "moon", "earthrise", "marssys", "mars", "marsurf1", "marsurf2",
         "marsurf3", "marsurf4", "phobos", "deimos", "emmars", "gaspra", "ida",
         "cv", "jupsys", "jupiter", "jupclose", "metis", "adrastea",
         "amalthea", "thebe", "io", "europa", "ganymede", "callisto", "satsys",
         "saturn", "pan", "atlas", "prometheus", "pandora", "epimetheus",
         "janus", "mimas", "enceladus", "tethys", "telesto", "calypso",
         "dione", "helene", "rhea", "titan", "titsurf", "hyperion", "iapetus",
         "phoebe", "urasys", "uranus", "puck", "miranda", "ariel", "umbriel",
         "titania", "oberon", "nepsys", "neptune", "naiad", "thalassa",
         "despina", "galatea", "larissa", "proteus", "triton", "nereid",
         "quaoar", "plutosys", "pluto", "makemake", "eris", "sedna", "kb",
         "halley", "halnuc", "oort", "edge"
      };

      final ImageView[] ivArray = new ImageView[names.length];
      for (int i = 0; i < names.length; i++)
         ivArray[i] = new ImageView(new Image(names[i]+".jpg"));

      ivScene0.addListener(new ChangeListener<ImageView>()
      {
         public void changed(ObservableValue<? extends ImageView> ov,
                             ImageView imOld, ImageView imNew)
         {
            root.getChildren().setAll(ivScene0.get(), ivScene1.get());
         }
      });

      ivScene1.set(ivArray[0]);
      ivScene0.set(ivArray[1]);

      final FadeTransition fadeout = new FadeTransition();
      fadeout.nodeProperty().bind(ivScene1);
      fadeout.setFromValue(1.0);
      fadeout.setToValue(0.0);
      fadeout.setDuration(Duration.valueOf("2000ms"));
      EventHandler<ActionEvent> eh;
      eh = new EventHandler<ActionEvent>()
      {
         public void handle(ActionEvent ae)
         {
            ivScene1.set(ivScene0.get());
            ivScene0.set(ivArray[i]);
            ivScene0.get().setOpacity(1.0);
            if (++i == ivArray.length)
               i = 0;
         }
      };
      fadeout.setOnFinished(eh);

      timeline = new Timeline();
      timeline.setCycleCount(Timeline.INDEFINITE);
      KeyFrame[] kf = new KeyFrame[1];
      eh = new EventHandler<ActionEvent>()
      {
         public void handle(ActionEvent ae)
         {
            fadeout.playFromStart();
         }
      };
      kf[0] = new KeyFrame(Duration.valueOf("4000ms"), eh);
      timeline.getKeyFrames().addAll(kf);
   }

   private int i = 2;
   private Timeline timeline;
}

public class Slideshow extends Application
{
   public static void main(String[] args)
   {
      Application.launch(args);
   }

   private Model model;

   @Override
   public void start(Stage primaryStage)
   {
      primaryStage.setTitle("Solar System Slideshow");
      primaryStage.setWidth(510);
      primaryStage.setHeight(430);
      primaryStage.setResizable(false);

      Group root = new Group();
      Scene scene = new Scene(root, Color.BLACK);
      model = new Model(root);
      model.play();

      primaryStage.setScene(scene);
      primaryStage.show();
   }

   @Override
   public void stop()
   {
      model.stop();
   }
}

Listing 4: Slideshow.java (version 3)

As I currently understand JavaFX 2.0, it's not possible to translate content: bind model.ivScene such that the result includes a property and a bind() method call. However, the same effect can be achieved by using a change listener, as follows:

  1. Convert ivScene0 from ImageView to ObjectProperty<ImageView> type.
  2. Add a change listener to ivScene0 by invoking ObjectProperty's inherited void addListener(ChangeListener<? super T> listener) method with the change listener as an argument.

    The change listener is an instance of a class (e.g., an anonymous class) that implements the javafx.beans.value.ChangeListener<T> interface. Its void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) method is invoked whenever the observable value changes. This listener executes root.getChildren().setAll(ivScene0.get(), ivScene1.get()); to replace the previous pair of ImageView nodes with the new pair.

  3. Execute ivScene1.set(ivArray[0]); followed by ivScene0.set(ivArray[1]);. This order ensures that ivScene1's value is set before ivScene0.set(ivArray[1]); causes changed() to be invoked, which results in ivScene1's value being retrieved in addition to ivScene0's value being retrieved.

The change listener eliminates the root.getChildren().setAll(ivScene0.get(), ivScene1.get()); method call from the event handler's handle() method in Model's constructor, from Slideshow's start() method, and from other places in future updates.

Enhancing Slideshow with Captions

We can improve the JavaFX 2.0 Slideshow application by adding an identifying caption (as a text node over a translucent rectangle that helps the text stand out) to the bottom center of each image. Listing 5 presents this improvement; changes between it and Listing 4 appear in red.

// Slideshow.java

import javafx.animation.FadeTransition;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;

import javafx.application.Application;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;

import javafx.event.ActionEvent;
import javafx.event.EventHandler;

import javafx.geometry.VPos;

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

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

import javafx.scene.paint.Color;

import javafx.scene.shape.Rectangle;

import javafx.scene.text.Font;
import javafx.scene.text.Text;

import javafx.stage.Stage;

import javafx.util.Duration;

class Model
{
   private ObjectProperty<Group> ivScene0 = new SimpleObjectProperty<Group>();
   private ObjectProperty<Group> ivScene1 = new SimpleObjectProperty<Group>();

   void play()
   {
      timeline.play();
   }

   void stop()
   {
      timeline.stop();
   }

   Model(final Group root, Scene scene)
   {
      String[] names = new String[]
      {
         "sol", "mercury", "mercsurf", "venus", "vensurf", "earthsys", "earth",
         "iss", "moon", "earthrise", "marssys", "mars", "marsurf1", "marsurf2",
         "marsurf3", "marsurf4", "phobos", "deimos", "emmars", "gaspra", "ida",
         "cv", "jupsys", "jupiter", "jupclose", "metis", "adrastea",
         "amalthea", "thebe", "io", "europa", "ganymede", "callisto", "satsys",
         "saturn", "pan", "atlas", "prometheus", "pandora", "epimetheus",
         "janus", "mimas", "enceladus", "tethys", "telesto", "calypso",
         "dione", "helene", "rhea", "titan", "titsurf", "hyperion", "iapetus",
         "phoebe", "urasys", "uranus", "puck", "miranda", "ariel", "umbriel",
         "titania", "oberon", "nepsys", "neptune", "naiad", "thalassa",
         "despina", "galatea", "larissa", "proteus", "triton", "nereid",
         "quaoar", "plutosys", "pluto", "makemake", "eris", "sedna", "kb",
         "halley", "halnuc", "oort", "edge"
      };

      String[] captions = new String[]
      {
         "Sol", "Mercury", "Mercury Surface", "Venus", "Venus Surface",
         "Terran System", "Earth", "International Space Station", "Luna",
         "Earthrise", "Martian System", "Mars", "Martian Surface",
         "Martian Surface", "Martian Surface", "Martian Surface", "Phobos",
         "Deimos", "Terran System Viewed From Mars", "Gaspra", "Ida",
         "Ceres | Vesta", "Jovian System", "Jupiter", "Jupiter Closeup",
         "Metis", "Adrastea", "Amalthea", "Thebe", "Io", "Europa", "Ganymede",
         "Callisto", "Saturnian System", "Saturn", "Pan", "Atlas", 
         "Prometheus", "Pandora", "Epimetheus", "Janus", "Mimas", "Enceladus",
         "Tethys", "Telesto", "Calypso", "Dione", "Helene", "Rhea", "Titan",
         "Titan Surface", "Hyperion", "Iapetus", "Phoebe", "Uranian System",
         "Uranus", "Puck", "Miranda", "Ariel", "Umbriel", "Titania", "Oberon",
         "Neptunian System", "Neptune", "Naiad", "Thalassa", "Despina",
         "Galatea", "Larissa", "Proteus", "Triton", "Nereid", "Quaoar",
         "Plutonian System", "Pluto", "MakeMake", "Eris System", "Sedna",
         "Kuiper Belt", "Halley's Comet", "Halley Nucleus", "Oort Cloud",
         "Solar System Edge"
      };

      final Group[] ivArray = new Group[names.length];
      for (int i = 0; i < names.length; i++)
      {
         ivArray[i] = new Group();
         ImageView iv = new ImageView(new Image(names[i]+".jpg"));
         ivArray[i].getChildren().add(new ImageView(new Image(names[i]+
                                                              ".jpg")));
         Text text = new Text(captions[i]);
         text.setFill(Color.WHITE);
         text.setFont(new Font("Arial", 20.0));
         text.setTextOrigin(VPos.TOP);
         Rectangle rect = new Rectangle();
         rect.setOpacity(0.5);
         DoubleProperty dp;
         dp = new SimpleDoubleProperty(text.boundsInLocalProperty().getValue()
                                           .getWidth()+10);
         rect.widthProperty().bind(dp);
         dp = new SimpleDoubleProperty(text.boundsInLocalProperty().getValue()
                                           .getHeight()+10);
         rect.heightProperty().bind(dp);
         rect.xProperty().bind(scene.widthProperty()
                                    .subtract(rect.widthProperty().getValue())
                                    .divide(2));
         rect.yProperty().bind(scene.heightProperty()
                                    .subtract(rect.heightProperty()
                                                  .getValue()+20));
         ivArray[i].getChildren().add(rect);
         text.xProperty().bind(rect.xProperty().add(5));
         text.yProperty().bind(rect.yProperty().add(5));
         ivArray[i].getChildren().add(text);
      }

      ivScene0.addListener(new ChangeListener<Group>()
      {
         public void changed(ObservableValue<? extends Group> ov,
                             Group imOld, Group imNew)
         {
            root.getChildren().setAll(ivScene0.get(), ivScene1.get());
         }
      });

      ivScene1.set(ivArray[0]);
      ivScene0.set(ivArray[1]);

      final FadeTransition fadeout = new FadeTransition();
      fadeout.nodeProperty().bind(ivScene1);
      fadeout.setFromValue(1.0);
      fadeout.setToValue(0.0);
      fadeout.setDuration(Duration.valueOf("2000ms"));
      EventHandler<ActionEvent> eh;
      eh = new EventHandler<ActionEvent>()
      {
         public void handle(ActionEvent ae)
         {
            ivScene1.set(ivScene0.get());
            ivScene0.set(ivArray[i]);
            ivScene0.get().setOpacity(1.0);
            if (++i == ivArray.length)
               i = 0;
         }
      };
      fadeout.setOnFinished(eh);

      timeline = new Timeline();
      timeline.setCycleCount(Timeline.INDEFINITE);
      KeyFrame[] kf = new KeyFrame[1];
      eh = new EventHandler<ActionEvent>()
      {
         public void handle(ActionEvent ae)
         {
            fadeout.playFromStart();
         }
      };
      kf[0] = new KeyFrame(Duration.valueOf("4000ms"), eh);
      timeline.getKeyFrames().addAll(kf);
   }

   private int i = 2;
   private Timeline timeline;
}

public class Slideshow extends Application
{
   public static void main(String[] args)
   {
      Application.launch(args);
   }

   private Model model;

   @Override
   public void start(Stage primaryStage)
   {
      primaryStage.setTitle("Solar System Slideshow");
      primaryStage.setWidth(510);
      primaryStage.setHeight(430);
      primaryStage.setResizable(false);

      Group root = new Group();
      Scene scene = new Scene(root, Color.BLACK);
      model = new Model(root, scene);
      model.play();

      primaryStage.setScene(scene);
      primaryStage.show();
   }

   @Override
   public void stop()
   {
      model.stop();
   }
}

Listing 5: Slideshow.java (version 4)

Listing 5 changes ObjectProperty's type from ImageView to Group because each scene element now consists of a group of nodes (a text node over a rectangle node over an image view node) rather than a single image view node.

Note: When nodes are added to a group, the first node to be added becomes the bottommost node and the last node to be added becomes the topmost node. The topmost node is displayed over everything else.

The Model constructor's for loop creates a separate Group node for each image name. This node stores an ImageView node, followed by a javafx.scene.shape.Rectangle node, followed by a javafx.scene.text.Text node.

The Text node's fill, font, text origin, x, and y properties are initialized by calling various Text methods. For example, void setTextOrigin(VPos value) is called to move the text's baseline to the top, which helps in positioning the text over the rectangle.

The Rectangle node's opacity, width, height, x, and y properties are initialized by calling various Rectangle methods. For example, void setOpacity(double value) is called to give the rectangle 50% opacity so that the background image partly shows through.

Text's position and Rectangle's position and dimension properties are bound to other properties so that the text is always placed over a translucent rectangle that's wide enough for the text, and so that the rectangle is always horizontally centered near the bottom of the image.

For example, text.xProperty().bind(rect.xProperty().add(5)); binds Text's x property to a Fluent API expression that consists of Rectangle's x property added to 5 -- adding 5 to the x property value allows room for a 5-pixel margin.

Figure 2 reveals an image with a caption identifier.

Figure 2: Earth and Luna are the distinguished members of the Terran System.

Enhancing Slideshow with Music

We can also improve the JavaFX 2.0 Slideshow application by adding music (e.g., a selection from Holst's Planets Suite -- see http://en.wikipedia.org/wiki/The_Planets) to the slideshow. Listing 6 presents this improvement; changes between it and Listing 5 appear in red

// Slideshow.java

import javafx.animation.FadeTransition;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;

import javafx.application.Application;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;

import javafx.event.ActionEvent;
import javafx.event.EventHandler;

import javafx.geometry.VPos;

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

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

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

import javafx.scene.paint.Color;

import javafx.scene.shape.Rectangle;

import javafx.scene.text.Font;
import javafx.scene.text.Text;

import javafx.stage.Stage;

import javafx.util.Duration;

class Model
{
   private ObjectProperty<Group> ivScene0 = new SimpleObjectProperty<Group>();
   private ObjectProperty<Group> ivScene1 = new SimpleObjectProperty<Group>();

   void play()
   {
      timeline.play();
   }

   void stop()
   {
      timeline.stop();
   }

   Model(final Group root, Scene scene)
   {
      String[] names = new String[]
      {
         "sol", "mercury", "mercsurf", "venus", "vensurf", "earthsys", "earth",
         "iss", "moon", "earthrise", "marssys", "mars", "marsurf1", "marsurf2",
         "marsurf3", "marsurf4", "phobos", "deimos", "emmars", "gaspra", "ida",
         "cv", "jupsys", "jupiter", "jupclose", "metis", "adrastea",
         "amalthea", "thebe", "io", "europa", "ganymede", "callisto", "satsys",
         "saturn", "pan", "atlas", "prometheus", "pandora", "epimetheus",
         "janus", "mimas", "enceladus", "tethys", "telesto", "calypso",
         "dione", "helene", "rhea", "titan", "titsurf", "hyperion", "iapetus",
         "phoebe", "urasys", "uranus", "puck", "miranda", "ariel", "umbriel",
         "titania", "oberon", "nepsys", "neptune", "naiad", "thalassa",
         "despina", "galatea", "larissa", "proteus", "triton", "nereid",
         "quaoar", "plutosys", "pluto", "makemake", "eris", "sedna", "kb",
         "halley", "halnuc", "oort", "edge"
      };

      String[] captions = new String[]
      {
         "Sol", "Mercury", "Mercury Surface", "Venus", "Venus Surface",
         "Terran System", "Earth", "International Space Station", "Luna",
         "Earthrise", "Martian System", "Mars", "Martian Surface",
         "Martian Surface", "Martian Surface", "Martian Surface", "Phobos",
         "Deimos", "Terran System Viewed From Mars", "Gaspra", "Ida",
         "Ceres | Vesta", "Jovian System", "Jupiter", "Jupiter Closeup",
         "Metis", "Adrastea", "Amalthea", "Thebe", "Io", "Europa", "Ganymede",
         "Callisto", "Saturnian System", "Saturn", "Pan", "Atlas", 
         "Prometheus", "Pandora", "Epimetheus", "Janus", "Mimas", "Enceladus",
         "Tethys", "Telesto", "Calypso", "Dione", "Helene", "Rhea", "Titan",
         "Titan Surface", "Hyperion", "Iapetus", "Phoebe", "Uranian System",
         "Uranus", "Puck", "Miranda", "Ariel", "Umbriel", "Titania", "Oberon",
         "Neptunian System", "Neptune", "Naiad", "Thalassa", "Despina",
         "Galatea", "Larissa", "Proteus", "Triton", "Nereid", "Quaoar",
         "Plutonian System", "Pluto", "MakeMake", "Eris System", "Sedna",
         "Kuiper Belt", "Halley's Comet", "Halley Nucleus", "Oort Cloud",
         "Solar System Edge"
      };

      final Group[] ivArray = new Group[names.length];
      for (int i = 0; i < names.length; i++)
      {
         ivArray[i] = new Group();
         ImageView iv = new ImageView(new Image(names[i]+".jpg"));
         ivArray[i].getChildren().add(new ImageView(new Image(names[i]+
                                                              ".jpg")));
         Text text = new Text(captions[i]);
         text.setFill(Color.WHITE);
         text.setFont(new Font("Arial", 20.0));
         text.setTextOrigin(VPos.TOP);
         Rectangle rect = new Rectangle();
         rect.setOpacity(0.5);
         DoubleProperty dp;
         dp = new SimpleDoubleProperty(text.boundsInLocalProperty().getValue()
                                           .getWidth()+10);
         rect.widthProperty().bind(dp);
         dp = new SimpleDoubleProperty(text.boundsInLocalProperty().getValue()
                                           .getHeight()+10);
         rect.heightProperty().bind(dp);
         rect.xProperty().bind(scene.widthProperty()
                                    .subtract(rect.widthProperty().getValue())
                                    .divide(2));
         rect.yProperty().bind(scene.heightProperty()
                                    .subtract(rect.heightProperty()
                                                  .getValue()+20));
         ivArray[i].getChildren().add(rect);
         text.xProperty().bind(rect.xProperty().add(5));
         text.yProperty().bind(rect.yProperty().add(5));
         ivArray[i].getChildren().add(text);
      }

      ivScene0.addListener(new ChangeListener<Group>()
      {
         public void changed(ObservableValue<? extends Group> ov,
                             Group imOld, Group imNew)
         {
            root.getChildren().setAll(ivScene0.get(), ivScene1.get());
         }
      });

      ivScene1.set(ivArray[0]);
      ivScene0.set(ivArray[1]);

      final FadeTransition fadeout = new FadeTransition();
      fadeout.nodeProperty().bind(ivScene1);
      fadeout.setFromValue(1.0);
      fadeout.setToValue(0.0);
      fadeout.setDuration(Duration.valueOf("2000ms"));
      EventHandler<ActionEvent> eh;
      eh = new EventHandler<ActionEvent>()
      {
         public void handle(ActionEvent ae)
         {
            ivScene1.set(ivScene0.get());
            ivScene0.set(ivArray[i]);
            ivScene0.get().setOpacity(1.0);
            if (++i == ivArray.length)
               i = 0;
         }
      };
      fadeout.setOnFinished(eh);

      timeline = new Timeline();
      timeline.setCycleCount(Timeline.INDEFINITE);
      KeyFrame[] kf = new KeyFrame[1];
      eh = new EventHandler<ActionEvent>()
      {
         public void handle(ActionEvent ae)
         {
            fadeout.playFromStart();
         }
      };
      kf[0] = new KeyFrame(Duration.valueOf("4000ms"), eh);
      timeline.getKeyFrames().addAll(kf);
   }

   private int i = 2;
   private Timeline timeline;
}

public class Slideshow extends Application
{
   public static void main(String[] args)
   {
      Application.launch(args);
   }

   private Model model;
   private MediaPlayer mp;

   @Override
   public void start(Stage primaryStage)
   {
      primaryStage.setTitle("Solar System Slideshow");
      primaryStage.setWidth(510);
      primaryStage.setHeight(430);
      primaryStage.setResizable(false);

      Group root = new Group();
      Scene scene = new Scene(root, Color.BLACK);
      model = new Model(root, scene);
      model.play();

      primaryStage.setScene(scene);
      primaryStage.show();

      Media media = new Media("http://tutortutor.ca/software/ASD/neptune.mp3");
      mp = new MediaPlayer(media);
      mp.play();
   }

   @Override
   public void stop()
   {
      mp.stop();
      model.stop();
   }
}

Listing 6: Slideshow.java (version 5)

Listing 6 uses the javafx.scene.media.Media and javafx.scene.media.MediaPlayer classes to play media. The former class represents a media resource. Its constructor accepts a String-based URI to the appropriate resource.

After instantiating Media, the start() method instantiates MediaPlayer, passing the Media instance to MediaPlayer's constructor. The returned MediaPlayer instance's void play() method is invoked to start playing the music.

MediaPlayer doesn't present a user interface for displaying media playback, which is appropriate for this example. If you need a user interface, you must instantiate the javafx.scene.media.MediaView class -- doing so is beyond the scope of this tutorial.

Making Waves

I've been involved with JavaFX since the first JavaFX software development kit, known as the JavaFX Preview SDK, debuted in summer 2007. Extending this technology with new features has been my favorite JavaFX activity.

Caution: To extend JavaFX, one must work with undocumented features that may change in subsequent releases of this technology. As a result, what works on one JavaFX version may fail to work on a future JavaFX version.

When JavaFX 2.0 debuted, I decided to extend it with a new effect, and created a simple Wave effect whose behavior is shown in Figure 3.

Figure 3: Sine wave-distorted text.

Figure 3 contrasts undistorted text on the left with Wave-distorted text on the right. Wave uses a sine wave to modify content position without affecting its color(s). A future configurable version of Wave would support different kinds of waves.

Before I could implement Wave, I needed to learn about JavaFX 2.0's effects architecture. I used a decompiler and a disassembler to explore the effects-related classfiles that are located in jfxrt.jar (JavaFX 2.0 runtime).

I discovered that JavaFX 2.0's effects architecture depends on shaders, small programs that calculate rendering effects on graphics hardware. Shaders are used to program a video card's graphics processing unit (GPU) programmable rendering pipeline.

I also discovered that I would need to create the following source files (prefixed by their package directory hierarchies) to completely implement the Wave effect:

  • javafx\scene\effect\Wave.java describes the main class (Wave) for accessing the Wave effect. This is the class that you instantiate when you invoke the Node class's void setEffect(Effect value) method. Wave declares several impl_-prefixed methods (among other methods) that the JavaFX runtime invokes when necessary. For example, the runtime invokes the com.sun.scenario.effect.Wave impl_createImpl() method to instantiate and return an instance of the com.sun.scenario.effect.Wave class, which is described next.
  • com\sun\scenario\effect\Wave.java describes a graphics pipeline-independent Wave implementation class. A notable feature of this class is the public Wave(Effect effect) constructor's updatePeerKey("Wave"); method call, which tells the JavaFX runtime that Wave peer classes (which are central to different JavaFX rendering pipelines) include the word Wave in their names.
  • com\sun\scenario\effect\impl\hw\d3d\hlsl\Wave.hlsl contains the shader source code for the Wave effect. This source code is written in Microsoft's High Level Shader Language (HLSL).
  • com\sun\scenario\effect\impl\prism\ps\PPSWavePeer.java describes the Prism DirectX pipeline peer class for rendering the Wave effect. This class's Shader createShader() method is called by the JavaFX runtime to create the shader. This method creates a couple of hashmaps to store samplers (types that represent textures and are used for texture sampling) and sampler parameters. The samplers hashmap identifies a sampler named baseimg, which suggests that the node's content is treated as a texture to be sampled. The parameters hashtable is empty because Wave is not parameterized. createShader() then obtains a reference to the current renderer and tells the renderer to create the Wave shader based on the hashtable entries.
  • com\sun\scenario\effect\impl\sw\java\JSWWavePeer.java describes the Prism Java software pipeline peer class for rendering the Wave effect. This version serves as a fallback in case the necessary DirectX environment to support Prism, or the CPU/SSE pipeline (described next) is not available.
  • com\sun\scenario\effect\impl\sw\sse\SSEWavePeer.java describes the CPU/SSE pipeline peer class for rendering the Wave effect via Intel's CPU-based Streaming SIMD Extensions (SSE).

Note: I could also create a javafx\builders\WaveBuilder.java source file to provide a JavaFX builder for the Wave class. However, I chose not to do so because Wave is very simple. If I develop a parameterized version of Wave, I'll probably include WaveBuilder. See Osvaldo Pinali Doederlein's JavaFX 2.0 Beta: First impressions blog post for a brief introduction to JavaFX builders.

It appears that JavaFX first attempts to use Prism/DirectX. If that pipeline isn't available, it attempts to use CPU/SSE. If that pipeline isn't available, it uses Prism/Java. I found no evidence of an OpenGL pipeline; it will probably be included in the final JavaFX 2.0 release.

For brevity, I implemented Wave using only the first four files in the previous list. As a result, Wave only supports the Prism/DirectX pipeline. If this pipeline isn't available on your Windows platform, any software that uses Wave will probably result in a thrown exception/error.

I then created a wave.jar file that is included with this tutorial's code. In addition to containing all necessary classfiles and Wave.obj (the compiled version of Wave.hlsl), this JAR file contains the source files and a Window batch file for compiling Wave.hlsl.

Note: I compiled wave.jar's source files using the Java 7 compiler (build 142). I compiled wave.hlsl to wave.obj using Microsoft's fxc.exe compiler program. fxc is distributed with the DirectX SDK.

Let's modify Listing 6's Slideshow.java source code to apply the Wave effect to each of the slideshow's images but not also to the captions. Listing 7 presents the final source code for this application; changes between it and Listing 6 are highlighted in red.

// Slideshow.java

import javafx.animation.FadeTransition;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;

import javafx.application.Application;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;

import javafx.event.ActionEvent;
import javafx.event.EventHandler;

import javafx.geometry.VPos;

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

import javafx.scene.effect.Wave;

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

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

import javafx.scene.paint.Color;

import javafx.scene.shape.Rectangle;

import javafx.scene.text.Font;
import javafx.scene.text.Text;

import javafx.stage.Stage;

import javafx.util.Duration;

class Model
{
   private ObjectProperty<Group> ivScene0 = new SimpleObjectProperty<Group>();
   private ObjectProperty<Group> ivScene1 = new SimpleObjectProperty<Group>();

   void play()
   {
      timeline.play();
   }

   void stop()
   {
      timeline.stop();
   }

   Model(final Group root, Scene scene)
   {
      String[] names = new String[]
      {
         "sol", "mercury", "mercsurf", "venus", "vensurf", "earthsys", "earth",
         "iss", "moon", "earthrise", "marssys", "mars", "marsurf1", "marsurf2",
         "marsurf3", "marsurf4", "phobos", "deimos", "emmars", "gaspra", "ida",
         "cv", "jupsys", "jupiter", "jupclose", "metis", "adrastea",
         "amalthea", "thebe", "io", "europa", "ganymede", "callisto", "satsys",
         "saturn", "pan", "atlas", "prometheus", "pandora", "epimetheus",
         "janus", "mimas", "enceladus", "tethys", "telesto", "calypso",
         "dione", "helene", "rhea", "titan", "titsurf", "hyperion", "iapetus",
         "phoebe", "urasys", "uranus", "puck", "miranda", "ariel", "umbriel",
         "titania", "oberon", "nepsys", "neptune", "naiad", "thalassa",
         "despina", "galatea", "larissa", "proteus", "triton", "nereid",
         "quaoar", "plutosys", "pluto", "makemake", "eris", "sedna", "kb",
         "halley", "halnuc", "oort", "edge"
      };

      String[] captions = new String[]
      {
         "Sol", "Mercury", "Mercury Surface", "Venus", "Venus Surface",
         "Terran System", "Earth", "International Space Station", "Luna",
         "Earthrise", "Martian System", "Mars", "Martian Surface",
         "Martian Surface", "Martian Surface", "Martian Surface", "Phobos",
         "Deimos", "Terran System Viewed From Mars", "Gaspra", "Ida",
         "Ceres | Vesta", "Jovian System", "Jupiter", "Jupiter Closeup",
         "Metis", "Adrastea", "Amalthea", "Thebe", "Io", "Europa", "Ganymede",
         "Callisto", "Saturnian System", "Saturn", "Pan", "Atlas", 
         "Prometheus", "Pandora", "Epimetheus", "Janus", "Mimas", "Enceladus",
         "Tethys", "Telesto", "Calypso", "Dione", "Helene", "Rhea", "Titan",
         "Titan Surface", "Hyperion", "Iapetus", "Phoebe", "Uranian System",
         "Uranus", "Puck", "Miranda", "Ariel", "Umbriel", "Titania", "Oberon",
         "Neptunian System", "Neptune", "Naiad", "Thalassa", "Despina",
         "Galatea", "Larissa", "Proteus", "Triton", "Nereid", "Quaoar",
         "Plutonian System", "Pluto", "MakeMake", "Eris System", "Sedna",
         "Kuiper Belt", "Halley's Comet", "Halley Nucleus", "Oort Cloud",
         "Solar System Edge"
      };

      final Group[] ivArray = new Group[names.length];
      for (int i = 0; i < names.length; i++)
      {
         ivArray[i] = new Group();
         ImageView iv = new ImageView(new Image(names[i]+".jpg"));
         iv.setEffect(new Wave());
         ivArray[i].getChildren().add(iv);
         Text text = new Text(captions[i]);
         text.setFill(Color.WHITE);
         text.setFont(new Font("Arial", 20.0));
         text.setTextOrigin(VPos.TOP);
         Rectangle rect = new Rectangle();
         rect.setOpacity(0.5);
         DoubleProperty dp;
         dp = new SimpleDoubleProperty(text.boundsInLocalProperty().getValue()
                                           .getWidth()+10);
         rect.widthProperty().bind(dp);
         dp = new SimpleDoubleProperty(text.boundsInLocalProperty().getValue()
                                           .getHeight()+10);
         rect.heightProperty().bind(dp);
         rect.xProperty().bind(scene.widthProperty()
                                    .subtract(rect.widthProperty().getValue())
                                    .divide(2));
         rect.yProperty().bind(scene.heightProperty()
                                    .subtract(rect.heightProperty()
                                                  .getValue()+20));
         ivArray[i].getChildren().add(rect);
         text.xProperty().bind(rect.xProperty().add(5));
         text.yProperty().bind(rect.yProperty().add(5));
         ivArray[i].getChildren().add(text);
      }

      ivScene0.addListener(new ChangeListener<Group>()
      {
         public void changed(ObservableValue<? extends Group> ov,
                             Group imOld, Group imNew)
         {
            root.getChildren().setAll(ivScene0.get(), ivScene1.get());
         }
      });

      ivScene1.set(ivArray[0]);
      ivScene0.set(ivArray[1]);

      final FadeTransition fadeout = new FadeTransition();
      fadeout.nodeProperty().bind(ivScene1);
      fadeout.setFromValue(1.0);
      fadeout.setToValue(0.0);
      fadeout.setDuration(Duration.valueOf("2000ms"));
      EventHandler<ActionEvent> eh;
      eh = new EventHandler<ActionEvent>()
      {
         public void handle(ActionEvent ae)
         {
            ivScene1.set(ivScene0.get());
            ivScene0.set(ivArray[i]);
            ivScene0.get().setOpacity(1.0);
            if (++i == ivArray.length)
               i = 0;
         }
      };
      fadeout.setOnFinished(eh);

      timeline = new Timeline();
      timeline.setCycleCount(Timeline.INDEFINITE);
      KeyFrame[] kf = new KeyFrame[1];
      eh = new EventHandler<ActionEvent>()
      {
         public void handle(ActionEvent ae)
         {
            fadeout.playFromStart();
         }
      };
      kf[0] = new KeyFrame(Duration.valueOf("4000ms"), eh);
      timeline.getKeyFrames().addAll(kf);
   }

   private int i = 2;
   private Timeline timeline;
}

public class Slideshow extends Application
{
   public static void main(String[] args)
   {
      Application.launch(args);
   }

   private Model model;
   private MediaPlayer mp;

   @Override
   public void start(Stage primaryStage)
   {
      primaryStage.setTitle("Solar System Slideshow");
      primaryStage.setWidth(510);
      primaryStage.setHeight(430);
      primaryStage.setResizable(false);

      Group root = new Group();
      Scene scene = new Scene(root, Color.BLACK);
      model = new Model(root, scene);
      model.play();

      primaryStage.setScene(scene);
      primaryStage.show();

      Media media = new Media("http://tutortutor.ca/software/ASD/neptune.mp3");
      mp = new MediaPlayer(media);
      mp.play();
   }

   @Override
   public void stop()
   {
      mp.stop();
      model.stop();
   }
}

Listing 7: Slideshow.java (version 6)

Assuming that you've installed the JavaFX 2.0 runtime to c:\progra~1\oracle\javafx 2.0 runtime, execute the following command sequence to compile Listing 7 and run the application:

javac -cp "c:\progra~1\oracle\javafx 2.0 runtime\lib\jfxrt.jar";wave.jar Slideshow.java
java -cp "c:\progra~1\oracle\javafx 2.0 runtime\lib\jfxrt.jar";wave.jar;. Slideshow

Figure 4 shows the result of this effect when applied to an image of Sol.

Figure 4: A watery sun.

Finally, let's take a look at the Wave shader's source code to see how this shader works -- see Listing 8.

sampler2D baseImg: register(s0);
void main(in float2 pos0: TEXCOORD0, in float2 pixcoord: VPOS, inout float4 color: COLOR0) 
{
   float2 p = pos0;
   p.y = pos0.y + sin(pos0.y * 100.0) * 0.03;
   color = tex2D(baseImg, p.xy);
}

Listing 8: Wave.hlsl

The shader's main() function is called repeatedly with X/Y texture coordinates specified by pos0. HLSL's sin() function is used to offset pos0's Y value, and its tex2D() function is used to calculate the color at the resulting X/Y position, which returns from main().

Exercises

  1. How do you specify the target node of a FadeTransition instance in a nonbinding context?
  2. How do you specify the target node of a FadeTransition instance in a binding context?
  3. How did I position the text node (in Listing 5) so that the text appears over the rectangle instead of above the rectangle?
  4. Does the MediaPlayer class present a visual user interface?
  5. JavaFX 2.0's effects architecture is based on what entity?

Code

You can download this post's code and answers here. Code (except for Wave effect) was developed and tested with JDK 7u2 and JavaFX SDK 2.0.2 on a Windows XP SP3 platform.

* * *

I welcome your input to this blog, and will write about relevant topics that you suggest. While waiting for the next blog post, check out my TutorTutor website to learn more about Java and other computer technologies (and that's just the beginning).