10 Minute Tutorial - JavaFX: Basic 2D Graphics and Animation
Update:
I have updated this tutorial for JavaFX 1.0
JavaFX, due to its declarative syntax, empowers developers with the ability to quickly build engaging user-interfaces that leverage eye-popping effects and animation to deliver a deeper, more visceral experience. But, you’ve got to learn to walk before you run
so this tutorial will explore JavaFX’s basic animation concepts by creating a simple slide show containing various shapes. For added fun, each slide will have the ability to rotate. While eye-popping it is not, I think this tutorial will give you a good starting point to begin working on more complex animations.
Prerequisites
- All the prerequisites defined in the
10 Minute JavaFX Tutorial - Develop and deploy JavaFX Applets and Applications while online and offline
QuickStart
If you want to see the end result of this tutorial and you have installed all the prerequisites, then please download the ZIP file below, unzip it and open the shapeslideshow-applet.html file in your browser.
- Shape Slide Show Sample (you may read this software’s license here)
If you want to see the sample in action right now, use the links below:
- Shape Slide Show JavaFX applet
- Shape Slide Show JavaFX Webstart application (you may need to save the JNLP file first, then open it)
Step 1: Define the slide show constants
First, we need to define several constants for the slide show. Create a file named “SlideShow.fx” and put this code in it:
package com.dieajax.sample.slideshow;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.animation.transition.RotateTransition;
import javafx.animation.transition.TranslateTransition;
import javafx.ext.swing.SwingButton;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.layout.HBox;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Ellipse;
import javafx.scene.text.Text;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
def width = 300;
def height = 200;
def slideWidth = 40;
def slideHeight = 40;
def startX = (width / 2) - (slideWidth / 2);
def startY = (height / 2) - (slideHeight / 2);
def nextX = width;
def nextY = (height / 2) - (slideHeight / 2);
I went ahead and added all the imports needed later, so you can ignore them for now. Skipping to the variable definitions , unsurprisingly, the “width” and “height” variables define the width and height of the slide show in pixels. “slideWidth” and “slideHeight” denote the width and height of a slide within the slide show itself. The “startX” and “startY” variables represent the upper-left coordiante of the first slide shown when the program starts. The upper-left coordinate for the rest of the slides, assigned to “nextX” and “nextY”, conviently puts the unseen slides “off-screen”, until brought into view by user later.
Step 2: Define the slide model
As I mentioned in in a previous JavaFX tutorial, the unique features of JavaFX really come into play when using MVC-like patterns, so this tutorial will model the slide show domain using classes. The first class, called SlideModel, will represent a slide itself:
...
class SlideModel {
var x: Number;
var y: Number;
var rotation: Number;
var rotateTransform = Rotate {
angle: bind rotation
pivotX: bind (x + slideWidth / 2 )
pivotY: bind (y + slideHeight /2 )
};
}
The “x”, “y” and “rotation” variables simply hold values for the position and rotation of the slide. The javafx.scene.transform.Rotate class represents a 2D rotation transform of a visual node in the JavaFX scene graph. Note that the Rotate class’ variables bind to variables defined in the enclosing SlideModel class. This means that if the “rotation” variable in the model gets updated, the Rotate transform updates, which will then make whatever visual node it applies to appear to spin.
If you don’t know about the bind statement or how JavaFX relates to a scene graph, please review my JavaFX deployment tutorial and JavaFX binding tutorial.
Step 3: Define the slide show model, part 1
Now, the actual slide show itself needs a defintion. Since this model class contains most of the application logic, I will go over it in sections. First, the variable defintions:
noclcik
class SlideShowModel {
def rectangleSlide = SlideModel {
x: startX;
y: startY;
}
def ellipseSlide = SlideModel {
x: nextX;
y: nextY;
}
def roundedRectangleSlide = SlideModel {
x: nextX;
y: nextY;
}
def slides: SlideModel[] = [rectangleSlide, ellipseSlide, roundedRectangleSlide];
var slideIndex: Number = 0;
var slideAnimationRunning: Boolean = false;
...
}
The slide show contains three slides each represented by their own SlideModel variable. The class also holds those three slides in a sequence named “slides” By indexing into the “slides” sequence , the “slideIndex” variable represents the currently displaying slide. Finally, the “slideAnimationRunning” variable simply represents whether or not slides are currently…sliding.
Step 4: Define the slide show model, part 2
With the variables out of the way, the functions come next:
class SlideShowModel {
...
function isAnimating() {
return slideAnimationRunning;
}
function getSlide( newSlideIndex:Integer ): SlideModel {
if( (newSlideIndex >= sizeof (slides)) or (newSlideIndex < 0) ) {
return null;
} else {
return slides[newSlideIndex];
}
}
function getPrevSlide(): SlideModel {
return getSlide( slideIndex - 1 );
}
function getCurrentSlide(): SlideModel {
return getSlide( slideIndex );
}
function getNextSlide(): SlideModel {
return getSlide( slideIndex + 1 );
}
...
}
These functions resemble read-only properties, rather than application logic functions, so they shouldn’t cause any comprehension problems. I highlighted the “return null;” statement merely to show that if the caller tries to use an out of bounds slide index, the “getSlide” function returns null. I also highlighted the names of the last three functions to point out that this class allows named access to the previous, current and next slide.
Step 5: Define the slide show model, part 3
Finally, the application logic. I’ll go through this a function at a time:
class SlideShowModel {
...
function rotate() {
if( isAnimating() ) {
return;
}
var slideRotationAnimation = Timeline {
keyFrames: [
KeyFrame {
time: 0s
values: getCurrentSlide().rotation => 0
action: function(): Void {
slideAnimationRunning = true;
}
}
KeyFrame {
time: 3s
values: getCurrentSlide().rotation => 720 tween Interpolator.EASEOUT
action: function(): Void {
slideAnimationRunning = false;
}
}
]
}
slideRotationAnimation.playFromStart();
}
...
}
The rotate function, obviously, rotates a slide. First, it makes sure all other animations have stopped. Then, it creates a javafx.animation.Timeline object, held in “slideRotationAnimation”, that defines the rotation (I will discuss this class in a moment). And finally, by calling the “playFromStart” function, current slide rotates.
The Timeline class represents an animation time line. You can think of animation as a bunch of pictures, or “frames”, displayed in rapid succession such that your eye creates smooth movement. Typically, an animator won’t draw each of these frames, they just define a beginning frame and an end frame then let the computer figure out all the frames in between. Animators call the beginning and end frames “key frames” and call the computational process the computer goes through to figure out the frames in between “tweening”. During the tweening process, animators can provide the computer with certain rules that define how it should create the frames in between. For example, the animator may want a square to move across the screen fast, but slow down as it gets to the end. So, the animator would tell the computer to interpolate bigger values at the beginning (to move it faster), and then smaller values at the end (to simulate it slowing down).
With those concepts in mind, let’s take another look at the “slideRotationAnimation” Timeline object. It defines two key frames on the time line, one at zero seconds the other at three seconds, meaning the animation will take three seconds to complete. The first frame initializes the rotation of the current slide to 0 and through the function stored in the “action” variable, sets “slideAnimationRunning” to true, so the slide show can track the running animation. The real magic happens in the definition of the last frame. Within the three seconds defined in its “time” variable, it increments the “rotation” variable of the current slide from zero (as defined in the first frame) to 720 (meaning, it rotates the square twice). Note that it uses an “EASEOUT” interpolation, meaning the rotation will slow down at the end. And finally, after the last frame completes, the function in the “action” variable gets called, telling the slide show that the animation has completed. To start the animation, we call its “playFromStart()” function, which returns immediately since animation happens asynchronously on another thread.
The Timeline class contains a “running” variable, allowing you to check the running state of an animation. In this particular case, the variable “slideRotationAnimation” goes out of scope, so I use the “slideAnimationRunning” variable to track the running state instead. Note that even though the Timeline object goes out of scope at the end of this function, the animation still runs.
The documentation for the KeyFrame class, says that the “values” variable contains a sequence of KeyValue instances. So, you may wonder exactly how the “=>” and “tween” syntax translates into an instantiation of a KeyValue object. I do too.
I couldn’t find any good documentation on this, so I would appreciate any information that could shed light on it.
Also, the documentation says that the functions defined in KeyFrame “action” variables occur AFTER the frame update. So, in this example, there exists a small chance two animations could overlap because the “slideAnimationRunning” variable wouldn’t get set until after the first frame occured. I left this issue in here to see if I could make it happen on my workstation and I only got it to happen once.
- Learn more about the Timeline class
- Learn more about the KeyFrame class
- Learn more about the Interpolator class
The rest of the functions follow the same pattern, so they won’t pose much of a problem. First, the “slideNext” function:
...
function slideNext() {
if( (slideShowModel.getNextSlide() == null) or isAnimating() ) {
return;
}
var slideNextAnimation = Timeline {
keyFrames: [
KeyFrame {
time: 0s
values: [getCurrentSlide().x => startX,
getNextSlide().x => nextX]
action: function(): Void {
slideAnimationRunning = true;
}
}
KeyFrame {
time: 1s
values: [getCurrentSlide().x => (-1 - slideWidth) tween Interpolator.EASEIN,
getNextSlide().x => startX tween Interpolator.EASEIN]
action: function(): Void {
slideAnimationRunning = false;
}
}
]
}
slideNextAnimation.playFromStart();
slideIndex++;
}
...
I highlighted the important parts. First, like the rotation function, it insures all other animations have stopped. Additionally, if it has reached the end of the slide show, it also returns. Since both the slide going out and the slide coming in need animating, the “values” variable in each KeyFrame contains two associations. Note, that the KeyFrames animate the “x” coordinate, now. Finally, the “slideIndex” variable gets incremented, since the slide show has moved to the next slide.
Updating “slideIndex” after the asynchronous “playFromStart” function won’t cause any problems because the KeyFrame works from the slide it got initialized with.
As you can imagine, the “slidePrevious” function works the same as “slideNext”:
...
function slidePrevious() {
if( (slideShowModel.getPrevSlide() == null) or isAnimating() ) {
return;
}
var slidePrevAnimation = Timeline {
keyFrames: [
KeyFrame {
time: 0s
values: [getCurrentSlide().x => startX,
getPrevSlide().x => (-1 - slideWidth)]
action: function(): Void {
slideAnimationRunning = true;
}
}
KeyFrame {
time: 1s
values: [getCurrentSlide().x => width + 1 tween Interpolator.EASEIN,
getPrevSlide().x => startX tween Interpolator.EASEIN]
action: function(): Void {
slideAnimationRunning = false;
}
}
]
}
slidePrevAnimation.playFromStart();
slideIndex--;
}
}
I won’t bore you by walking through this code, too. Just extrapolate from “slideNext”.
Now, let’s quickly instantiate an instance of the SlideShowModel class:
var slideShowModel = SlideShowModel {
}
Hey, I said quick.
All the class initialization happens in the class declaration, so this object literal assignment can remain empty.
Step 6: Define the view model
Following something akin to the Model-View-ViewModel paradigm, let’s create ViewModel class that bridges elements of the view to the model:
class ViewModel {
def rectangle = Rectangle {
width: 40
height: 40
fill: Color.GREEN
stroke: Color.BLACK
x: bind slideShowModel.rectangleSlide.x;
y: bind slideShowModel.rectangleSlide.y;
transforms: [slideShowModel.rectangleSlide.rotateTransform];
}
def ellipse = Ellipse {
radiusX: 20
radiusY: 13
fill: Color.BLUE
stroke: Color.BLACK
centerX: bind slideShowModel.ellipseSlide.x + (slideWidth / 2);
centerY: bind slideShowModel.ellipseSlide.y + (slideHeight / 2);
transforms: [slideShowModel.ellipseSlide.rotateTransform];
}
def roundedRectange = Rectangle {
width: 40
height: 40
arcHeight: 10
arcWidth: 10
fill: Color.ORANGE
stroke: Color.BLACK
x: bind slideShowModel.roundedRectangleSlide.x;
y: bind slideShowModel.roundedRectangleSlide.y;
transforms: [slideShowModel.roundedRectangleSlide.rotateTransform];
}
def shapes: Node[] = [rectangle,
ellipse,
roundedRectange];
}
Nothing too difficult to understand here, but I want to draw your attention to the highlighted lines. The first line assigns transforms performed on the Rectangle to the transforms inside of a the “rectangleSlide” SlideModel instance. Without this assignment, rotations wouldn’t display. It appears that whenever JavaFX sees an update to a transform stored in the “transforms” variable of a class derived from Node it updates that node on the screen. Also, remember that “rectangeSlide” binds the angle property of its “rotateTransform” variable to the rotation property, which gets animated in SlideShowModel.rotate. This effortless flow of data from model to view displays the true power of JavaFX’s declartive data binding.
I also highlighted the binding of the Ellipse’s position, simply to show that it needs an offset because developers have to position it by its center rather than upper-left coordinate like the rectangles.
Let’s instantiate this model, too:
var viewModel = ViewModel {
}
Step 7: Define the view
Last but not least, let’s create the Stage:
Stage {
title: "Die, Ajax! - Slide Show Sample"
width: 300
height: 300
visible: true
scene: Scene {
content: [
viewModel.shapes,
HBox {
content: [
SwingButton {
text: "<-"
action: function(): Void {
slideShowModel.slideNext();
}
},
SwingButton {
text: "Spin"
action: function(): Void {
slideShowModel.rotate();
}
},
SwingButton {
text: "->"
action: function(): Void {
slideShowModel.slidePrevious();
}
}
]
translateX: 83
translateY: 180
}
]
}
}
After going through my other tutorials, this shouldn’t cause you any problems. I highlighted the line where shapes get added for display. I also highlighted where SlideShowModel functions get called.
Step 8: Run it!
Without an IDE, building JavaFX applications for the desktop, browser and Webstart remains a slightly involved process. I explain this process step-by-step in my 10 Minute Tutorial - JavaFX: Hello World and 10 Minute JavaFX Tutorial - Develop and deploy JavaFX Applets and Applications while online and offline articles, so I will just walk you through the basics here. Feel free to reference those articles for more detail.
Compile the code
No matter the deployment platform, we need to compile the code first. This line takes care of it:
"C:\Program Files\JavaFX\javafx-sdk1.0\bin\javafxc.exe" -d . ShapeSlideShow.fx
Now, we’ll look at each platform individually.
Run the application on the desktop
We only need one command to do this:
"C:\Program Files\JavaFX\javafx-sdk1.0\bin\javafx.exe" com.dieajax.sample.slideShow.ShapeSlideShow
Run the application as an applet
Per my 10 Minute JavaFX Tutorial - Develop and deploy JavaFX Applets and Applications while online and offline article, first let’s make a JAR file:
"C:\Program Files\Java\jdk1.6.0_11\bin\jar.exe" cvf ShapeSlideShow.jar com\dieajax\sample\slideshow\ShapeSlideShow*.class
Can’t make it appear in the browser without some HTML, so let’s create an HTML file named “shapeslideshow-applet.html” and add that next:
<html>
<head>
<title>Shape Slide Show Applet</title>
</head>
<body>
<script src="http://dl.javafx.com/dtfx.js"></script>
<script>
javafx(
{
archive: "ShapeSlideShow.jar",
draggable: true,
width: 300,
height: 300,
code: "com.dieajax.sample.slideshow.ShapeSlideShow",
name: "ShapeSlideShow"
}
);
</script>
</body>
</html>
Finally, we need the applet JNLP file named “ShapeSlideShow_browser.jnlp”:
<?xml version="1.0" encoding="UTF-8"?>
<jnlp spec="1.0+">
<information>
<title>Shape Slide Show Sample</title>
<vendor>Acme Corporation</vendor>
<description>Shape Slide Show Sample Applet</description>
</information>
<resources>
<j2se version="1.5+"/>
<jar href="ShapeSlideShow.jar" main="true" download="eager"/>
<extension name="JavaFX Runtime" href="http://dl.javafx.com/javafx-rt.jnlp"/>
</resources>
<applet-desc main-class="com.sun.javafx.runtime.adapter.Applet" name="ShapeSlideShow" height="300" width="300">
<param name="MainJavaFXScript" value="com.dieajax.sample.slideshow.ShapeSlideShow"/>
</applet-desc>
</jnlp>
Test this by opening the “shapeslideshow-applet.html” file in your browser.
Run the application through Java Webstart
Deploying applications through Java Webstart requires HTML, too, so put this code inside of a new file named “shapeslideshow-webstart.html”:
<html>
<head>
<title>Shape Slide Show Webstart</title>
</head>
<body>
<a href="ShapeSlideShow.jnlp">Shape Slide Show Webstart</a>
</body>
</html>
Our Webstart JNLP file, stored in “ShapeSlideShow.jnlp” will look very similar to the applet version, but with a few tweaks:
<?xml version="1.0" encoding="UTF-8"?>
<jnlp spec="1.0+" codebase="file:/C:\Documents and Settings\David Miles\My Documents\javafx\slideshow">
<information>
<title>Shape Slide Show Sample</title>
<vendor>Acme Corporation</vendor>
<description>Shape Slide Show Sample Applet</description>
</information>
<resources>
<j2se version="1.5+"/>
<jar href="ShapeSlideShow.jar" main="true" download="eager"/>
<extension name="JavaFX Runtime" href="http://dl.javafx.com/javafx-rt.jnlp"/>
</resources>
<application-desc main-class="com.dieajax.sample.slideshow.ShapeSlideShow" />
</jnlp>
Remember to change the <jnlp> “codebase” attribute to match your disk or web server directory structure.
To run the application through Java Webstart you should only need to open the “shapeslideshow-webstart.html” file and click on the link.
Conclusion
Whew! Lots of code in this tutorial, but JavaFX’s declaritve variable binding and declartive animation helps keep the code easy and understandable. My example here only touched the tip of the iceberg of JavaFX animation. JavaFX provides the tools from developers to create considerably more complex animations that contain multiple, embedded time lines with the same ease I created them above. So, start playing with it and see what you can come up with.



