Monday, February 20, 2012

JavaFX 2: Simultaneous Animated Text Strings

I decided to have a simple JavaFX 2 animated sample running as attendees entered the room for my RMOUG Training Days 2012 presentation. The desired result was achieved as attendees asked if how the running application was implemented. More than one person was surprised that it was implemented in "pure Java" without Flash or HTML5. I provide the source code for this simple example along with some screen snapshots in this post. Along the way, the example demonstrates animating multiple instances of Text along a path simultaneously.

I have previously blogged about animating along a predefined path using PathTransition. I have adapted that example into this new example by changing the moving Shape from a Circle to a Text "shape" and by having three of these "shapes" move along the path simultaneously via ParallelTransition.

To add a little extra to the demonstration, I have also made use of JavaFX effects, font sizing and specification, and character coloring in this example. The code listing for the entire simple demonstrative sample is shown next.

RmougTd2012Animation.java
package dustin.examples;

import javafx.animation.ParallelTransition;
import javafx.animation.PathTransition;
import javafx.animation.PathTransition.OrientationType;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.effect.Glow;
import javafx.scene.effect.Reflection;
import javafx.scene.paint.Color;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.Shape;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextBuilder;
import javafx.stage.Stage;
import javafx.util.Duration;

/**
 * Simple animation of labels related to RMOUG Training Days 2012 using JavaFX.
 * 
 * @author Dustin
 */
public class RmougTd2012Animation extends Application
{
   /**
    * Generate Path upon which animation will occur.
    * 
    * @return Generated path.
    */
   private Path generateCurvyPath()
   {
      final Path path = new Path();
      path.getElements().add(new MoveTo(70,20));
      path.getElements().add(new CubicCurveTo(430, 0, 430, 120, 250, 120));
      path.getElements().add(new CubicCurveTo(50, 120, 50, 240, 430, 240));
      path.setOpacity(0.0);
      return path;
   }

   /**
    * Generate the path transition.
    * 
    * @param shape Shape to travel along path.
    * @param path Path to be traveled upon.
    * @param duration Duration of single animation.
    * @param delay Delay before beginning first animation.
    * @param orientation Orientation of shape during animation.
    * @return PathTransition.
    */
   private PathTransition generatePathTransition(
      final Shape shape, final Path path,
      final Duration duration, final Duration delay,
      final OrientationType orientation)
   {
      final PathTransition pathTransition = new PathTransition();
      pathTransition.setDuration(duration);
      pathTransition.setDelay(delay);
      pathTransition.setPath(path);
      pathTransition.setNode(shape);
      pathTransition.setOrientation(orientation);
      pathTransition.setCycleCount(Timeline.INDEFINITE);
      pathTransition.setAutoReverse(true);
      return pathTransition;
   }

   /**
    * Generate RMOUG text string with appropriate fill, font, and effect.
    * 
    * @return "RMOUG" text string with fill, font, and effect.
    */
   private Text generateRmougText()
   {
      return TextBuilder.create().text("RMOUG").x(20).y(20).fill(Color.DARKGRAY)
                        .font(Font.font(java.awt.Font.SERIF, 75))
                        .effect(new Glow(0.25)).build();
   }

   /**
    * Generate "Training Days 2012" text string with appropriate position, fill,
    * and font.
    * 
    * @return "Training Days 2012" with specified font, fill, and position.
    */
   private Text generateTrainingDaysText()
   {
      return TextBuilder.create().text("Training Days 2012")
                        .x(380).y(240).fill(Color.DARKOLIVEGREEN)
                        .font(Font.font(java.awt.Font.SERIF, 50)).build();
   }

   /**
    * Location String with specifed effect, font, and position.
    * 
    * @return Location String with specified effect, font, and position.
    */
   private Text generateDenverText()
   {
      final Reflection reflection = new Reflection();
      reflection.setFraction(1.0);
      return TextBuilder.create()
                        .text("Denver, Colorado").x(20).y(20)
                        .font(Font.font(java.awt.Font.SANS_SERIF, 25))
                        .effect(reflection)
                        .build();
   }

   /**
    * Apply animation.
    *  
    * @param group Group to which animation is to be applied.
    */
   private void applyAnimation(final Group group)
   {
      final Path path = generateCurvyPath();
      group.getChildren().add(path);
      final Shape rmoug = generateRmougText();
      group.getChildren().add(rmoug);
      final Shape td = generateTrainingDaysText();
      group.getChildren().add(td);
      final Shape denver = generateDenverText();
      group.getChildren().add(denver);
      final PathTransition rmougTransition =
         generatePathTransition(
            rmoug, path, Duration.seconds(8.0), Duration.seconds(0.5),
            OrientationType.NONE);
      final PathTransition tdTransition =
         generatePathTransition(
            td, path, Duration.seconds(5.5), Duration.seconds(0.1),
            OrientationType.NONE);
      final PathTransition denverTransition =
         generatePathTransition(
            denver, path, Duration.seconds(30), Duration.seconds(3),
            OrientationType.ORTHOGONAL_TO_TANGENT);
      final ParallelTransition parallelTransition =
         new ParallelTransition(rmougTransition, tdTransition, denverTransition);
      parallelTransition.play(); 
   }

   /**
    * JavaFX Application starting method.
    * 
    * @param stage Primary stage.
    * @throws Exception Potential JavaFX application exception.
    */
   @Override
   public void start(Stage stage) throws Exception
   {
      final Group rootGroup = new Group();
      final Scene scene = new Scene(rootGroup, 500, 400, Color.GHOSTWHITE);
      stage.setScene(scene);
      stage.setTitle("JavaFX 2 RMOUG Training Days 2012 Animations");
      stage.show();
      applyAnimation(rootGroup);
   }

   /**
    * Main function for running JavaFX animation demo.
    * 
    * @param arguments Command-line arguments; none expected.
    */
   public static void main(final String[] arguments)
   {
      Application.launch(arguments);
   }
}

The next series of screen snapshots attempt to provide an idea of how this appears when it is executed.

This example demonstrates that text can be animated along a path and it also demonstrates the difference between OrientationType types NONE and ORTHOGONAL_TO_TANGENT. The former orientation specification has the text facing upwards from an absolutist point of view while the latter shifts the text's direction according to the twists and turns of the path. The Javadoc for each type provides more precise explanation where NONE is defined as "The targeted node's rotation matrix stays unchange along the geometric path" while ORTHOGONAL_TO_TANGENT is defined as "The targeted node's rotation matrix is set to keep node perpendicular to the path's tangent along the geometric path."

For me, the more interesting part of this example is how easy it is to specify multiple animations be run in parallel. As the applyAnimation method of the sample code shows, three text Strings are associated with three distinct PathTransition instances. Although each of these three instances of PathTransition uses the same Path, the start time and duration time of each transition differs. None of the PathTransition instances have their individual play() methods called. Rather, each is associated with a single ParallelTransition instance and it is that ParallelTransition instance's play() method that is invoked. Because the three instances of PathTransition were associated with the instance of ParallelTransition, invoking play() on it invokes play() on all three individual PathTransition instances in parallel.

Conclusion

JavaFX 2 makes it easy to perform multiple transitions in parallel. One simply sets up each individual transition in a preferred manner and then associates each of these transitions with an instance of ParallelTransition and invokes the play() method on that instance of ParallelTransition. Animation is not limited to shapes, but can be used on text as well.

No comments: