10 Minute Tutorial - JavaFX: Basic 2D Graphics and Animation

My last JavaFX tutorial beared one part fruit and one part pain. Clearly, more bugs need squishing and more language features need exercising before JavaFX can even hope to call itself “beta” quality. But, each code drop does get better and better, and even now, I can tell that the end product will turn out nice. So…

…we will press on.(not the greatest movie, but that scene gets me everytime!)

JavaFX, due to its declarative syntax, promises to empower developers with the ability to quickly build engaging user-interfaces by leverage eye-popping effects and animations to deliver a deeper, more visceral experience. But, you’ve got to walk before you run so this tutorial will explore JavaFX’s basic animation concepts by creating a simple slideshow 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

QuickStart

If you want to see the result of this tutorial and you have installed all the prerequisites, then please download the ZIP file below, unzip it, open and edit the ShapeSlideShow.bat to match your directory structure and then run the ShapeSlideShow.bat file.

You can find a screen shot of the running application, here.

Step 1: Create the Slide model

Every good MVC application has a model, so we’ll start by modeling a “slide” in the slide show. Create a file named “ShapeSlideshow.fx” and declare a class named Slide like so:

import javafx.ui.*;
import javafx.ui.canvas.*;

class Slide extends Group {
    attribute x: Number;
    attribute y: Number;
    attribute rotation: Number;
}

The Group class which Slide inherits from gives all nodes contained within the group their own shared coordinate space. So, you can manipulate several UI elements at once by doing simple 2D transforms on the Group itself. You’ll see this in action later.

As you’d expect, changing the Slide class’ x and y attributes affect its screen position and the rotation attribute sets its degree of rotation. Nothing fancy here.

Step 2: Create the Slideshow model

Now, we’ll model an actual slideshow. This will look ugly initially, but it’ll all make sense later (I hope :) ):

class SlideShow {
    attribute width: Number;
    attribute height: Number;
    attribute slideIndex: Number;
    attribute currentSlide: Slide;
    attribute prevSlide: Slide;
    attribute nextSlide: Slide;
    attribute slideWidth: Number;
    attribute slideHeight: Number;
    attribute startX: Number;
    attribute startY: Number;
    attribute nextX: Number;
    attribute nextY: Number;
    attribute slides: Slide*;

    attribute isAnimating: Boolean;

    attribute currentStopX: Number;
    attribute nextStopX: Number;
    attribute currentSlideX: Number;
    attribute nextSlideX: Number;
    attribute prevSlideX: Number;
    attribute currentSlideRotation: Number;
    attribute animatingNext: Boolean;

    operation doNextAnimationStopped();
    operation doPrevAnimationStopped();
    operation getSlide( newSlideIndex: Number);
    operation slideNext();
    operation slidePrevious();
    operation rotate();
}

Yeah, it ain’t small. I won’t explain each of these attributes and operations right now. You’ll see how they get used as we go on. However, I do want to explain the highlighted attributes. Unfortunately, JavaFX currently lacks a way to signal the application when an animation has stopped. So, each of the highlighted attributes helps in determining when JavaFX’s animation loops have completed running. Hopefully, as JavaFX’s evolves, I can get rid of this housekeeping cruft.

Step 3: Instantiate and initialize the Slideshow

We’ll do this in pieces. First, to set some basic positions and dimensions:

attribute SlideShow.slideWidth = 40;
attribute SlideShow.slideHeight = 40;

var slideShow = SlideShow {
    var: self

    slideIndex: 0
    width: 300
    height: 200

    startX: self.width / 2 - self.slideWidth / 2
    startY: self.height / 2 - self.slideHeight / 2
    nextX: self.width
    nextY: self.height / 2 - self.slideHeight / 2

So, the first two lines say that the slideshow expects all slides to have a width and height of 40 pixels. Next, the width and height of the slideshow gets set to 300 and 200 pixels, respectively and the index of starting slide gets set to 0. The calculation for the startX and startY variables places the first slide smack in the middle of the 300×200 pixel dimensions specified earlier. NextX and nextY place the next slide(s) off screen, so they can “slide” into view when needed.

Now, to give the slideshow some slides:

    slides:
    [Slide {
        var: slideSelf

        x: self.startX
        y: self.startY

        content: Rect {
            width: 40
            height: 40
            fill: green
            stroke: black
        }
        transform: bind [translate(slideSelf.x,slideSelf.y), rotate(slideSelf.rotation,self.slideWidth/2,self.slideHeight/2)]
    },
      Slide {
        var: slideSelf

        x: self.nextX
        y: self.nextY

        content: Ellipse {
        	transform: [translate(self.slideWidth/2,self.slideHeight/2)]
            radiusX: 20
	radiusY: 13
            fill: blue
            stroke: black
	}

	transform: bind [translate(slideSelf.x,slideSelf.y), rotate(slideSelf.rotation,self.slideWidth/2,self.slideHeight/2)]

    },
    Slide {
        var: slideSelf

        x: self.nextX
        y: self.nextY
        content: Rect {
            width: 40
            height: 40
	arcHeight: 10
	arcWidth: 10
            fill: orange
            stroke: black
        }
        transform: bind [translate(slideSelf.x,slideSelf.y), rotate(slideSelf.rotation,self.slideWidth/2,self.slideHeight/2)]
    }]

This repetitive, self-explanatory code should read like a book to you by now. I did want to draw your attention to the highlighted sections, however. The first highlighted section points out that the first slide (slide index 0) gets positioned at startX and startY. The second highlighted section shows that all the following slides get set to the nextX and nextY positions (essentially waiting off-screen). Finally, I highlighted the last set of lines because we have yet to address the transform attribute shared by most UI elements.

In JavaFX, typically, you can apply a number of “transforms” to a UI element by assigning an array of Transform-derived objects to its transform attribute. I’ve included a few of them in the links below.

Note the bind statement in highlighted transform code I referenced earlier. This bind statement means that as the values for x, y and rotation change in our Slide objects, the objects within the Slide will also get transformed.

Speaking of bind, the Slideshow needs a few bind statements of its own:

    prevSlide: bind self.getSlide(self.slideIndex - 1)
    currentSlide: bind self.getSlide(self.slideIndex)
    nextSlide: bind self.getSlide(self.slideIndex + 1)

    currentSlideX: bind self.currentSlide.x
    nextSlideX: bind self.nextSlide.x
    prevSlideX: bind self.prevSlide.x
    currentSlideRotation: bind self.currentSlide.rotation

So, at all times we track the current, previous and next slides, which get calculated based on the current slideIndex (we’ll define the getSlide operation in a moment, although you can probably guess what it does). As I stated in Step 2, the next four attributes I use merely to detect when animations have stopped, an ability that JavaFX will hopefully gain at a later date. In any case, you’ll see how they work in a moment.

Step 4: Start animating slides

Now, the fun part! (Yaaayyyy! :) ) But first, (Awwww! :( ) I’ll define the getSlide function, as promised:

operation SlideShow.getSlide( newSlideIndex:Number ) {
    if( (newSlideIndex >= sizeof (slides)) or (newSlideIndex < 0) ) {
        return null;
    } else {
        return slides[newSlideIndex];
    }
}

Like I said, self-explanatory.

Moving on to something more interesting, below I pasted the functions which do the real work:

operation SlideShow.slideNext() {
    if( ((slideIndex + 1 ) == sizeof( slides )) or isAnimating) {
        return;
    }

   isAnimating = true;
    animatingNext = true;
    currentSlide.x = [currentSlide.x..-1-slideWidth] dur 1000 easein;
    nextSlide.x = [nextSlide.x..startX] dur 1000 easeout;
}

operation SlideShow.slidePrevious() {
    if( ((slideIndex - 1) < 0) or isAnimating ) {
        return;
    }

    isAnimating = true;
    animatingNext = false;
    currentSlide.x = [currentSlide.x..width + 1] dur 1000 easein;
    prevSlide.x = [prevSlide.x..startX] dur 1000 easeout;
}  

operation SlideShow.rotate() {
    if( isAnimating ) {
	return;
    }

    isAnimating = true;
    currentSlide.rotation = [0..720] dur 3000;
}

I’ll let you read through the code on your own, but I will say that each function follows a common pattern: determine if the current state of the application allows the animation to occur. If so, start the animation and set flags to ignore all input until it completes.

We did all this setup for one little operator within the line I highlighted: the dur operator. The dur operator (dur stands for duration) allows you set a variable to an entire range of values over the span of time. So, in the highlighted statement, currentSlide.x gets set to all the values between its current value and -1 – slideWidth over the span of 1000 milliseconds. The easein qualifier tells the JavaFX animation engine to start the animation slowly and then speed up over time. This makes the slides look like they accelerate as the fly off screen. All in one statement. Pretty neat, huh?

Step 5: Stop animating slides

Well, JavaFX doesn’t give us the ability to stop an animation directly just yet, but I hacked together something that will let me know when an animation has completed, at least:

operation SlideShow.doNextAnimationStopped() {
    if( (currentSlide.x == -1-slideWidth) and (nextSlide.x == startX)) {
        slideIndex++;
        isAnimating = false;
    }
}

operation SlideShow.doPrevAnimationStopped() {
    if( (currentSlide.x == width + 1) and (prevSlide.x == startX)) {
        slideIndex--;
        isAnimating = false;
    }
}

trigger on SlideShow.currentSlideX = newValue {
    if( animatingNext == true ) {
        doNextAnimationStopped();
    } else {
        doPrevAnimationStopped();
    }
}


trigger on SlideShow.nextSlideX = newValue {
    doNextAnimationStopped();
}

trigger on SlideShow.prevSlideX = newValue {
    doPrevAnimationStopped();
}

trigger on SlideShow.currentSlideRotation = newValue {
    if( currentSlide.rotation == 720 ) {
	isAnimating = false;
    }
}

The real key lies in the triggers. Remember, in Step 4, currentSlide.x got animated to push the slide off-screen. Well, in Step3, I bound currentSlideX to currentSlide.x so every time currentSlide.x changes (even while it animates), currentSlideX changes. So, in the highlighted code, I set a trigger on currentSlideX to call either doNextAnimationStopped or doPrevAnimationStopped (depending on if the user clicked the next or previous button). Both these functions check to see if both animating slides have reached their finish destinations and if so, updates the slide index and sets isAnimating to false thereby creating a poor man’s event callback system.

Trust me, I enjoyed programming that even less than you enjoyed reading the explanation.

Step 6: Create the UI (what little there is ;) )

Let’s start with the code:

Frame {
    title: "Die, Ajax! - Slide Show Sample"
    width: 300
    height: 300
    visible: true
    content:
    Panel {
        content: [
        Canvas {
            var: self
            x: 0
            y: 0
            width: 300
            height: 300
            content:
            [slideShow.slides,
            View {
                transform: [translate(63,180)]
                content: GroupPanel {
                    cursor: DEFAULT
                    var row = Row {alignment: BASELINE}
                    var column1 = Column { }
                    var column2 = Column { }
                    var column3 = Column { }
                    rows: [row]
                    columns: [column1, column2, column3]
                    content:
                    [Button {
                        row: row
                        column: column1
                        text: “<-”
                        action: operation() {
                            slideShow.slideNext();
                        }
                    },Button {
                        row: row
                        column: column2
                        text: “Spin”
                        action: operation() {
                            slideShow.rotate();
                        }
                    }, Button {
                        row: row
                        column: column3
                        text: “->”
                        action: operation() {
                            slideShow.slidePrevious();
                        }
                    }]
                }
            }]
        }]
    }
}

Most of this should read pretty easily by now. The statement I highlighted adds the slides to the Frame’s content list. It adds ALL the slides, even the non-visible ones. Those simply wait off-screen waiting to come into view. Let’s hope JavaFX’s clipper doesn’t take too much of a performance hit for my laziness ;).

Step 7: Run it!

All the code in its entirety:

import javafx.ui.*;
import javafx.ui.canvas.*;

class Slide extends Group {
    attribute x: Number;
    attribute y: Number;
    attribute rotation: Number;
}

class SlideShow {
	attribute width: Number;
	attribute height: Number;
    attribute slideIndex: Number;
    attribute currentSlide: Slide;
    attribute prevSlide: Slide;
    attribute nextSlide: Slide;
    attribute slideWidth: Number;
    attribute slideHeight: Number;
    attribute startX: Number;
    attribute startY: Number;
    attribute nextX: Number;
    attribute nextY: Number;
    attribute slides: Slide*;
    attribute isAnimating: Boolean;

    //Cruft to determine whether or not animation has stopped
    attribute currentStopX: Number;
    attribute nextStopX: Number;
    attribute currentSlideX: Number;
    attribute nextSlideX: Number;
    attribute prevSlideX: Number;
    attribute currentSlideRotation: Number;
    attribute animatingNext: Boolean;

    operation doNextAnimationStopped();
    operation doPrevAnimationStopped();
    operation getSlide( newSlideIndex: Number);
    operation slideNext();
    operation slidePrevious();
    operation rotate();
}

attribute SlideShow.slideWidth = 40;
attribute SlideShow.slideHeight = 40;

var slideShow = SlideShow {
    var: self

    slideIndex: 0
	width: 300
	height: 200

    startX: self.width / 2 - self.slideWidth / 2
    startY: self.height / 2 - self.slideHeight / 2
    nextX: self.width
    nextY: self.height / 2 - self.slideHeight / 2

    slides:
    [Slide {
		var: slideSelf

        x: self.startX
        y: self.startY
        content: Rect {
            width: 40
            height: 40
            fill: green
            stroke: black
        }
		transform: bind [translate(slideSelf.x,slideSelf.y), rotate(slideSelf.rotation,self.slideWidth/2,self.slideHeight/2)]
    },
		Slide {
		var: slideSelf

        x: self.nextX
        y: self.nextY
        content: Ellipse {
			transform: [translate(self.slideWidth/2,self.slideHeight/2)]
            radiusX: 20
            radiusY: 13
            fill: blue
            stroke: black
		}
		transform: bind [translate(slideSelf.x,slideSelf.y), rotate(slideSelf.rotation,self.slideWidth/2,self.slideHeight/2)]
    },
    Slide {
		var: slideSelf

        x: self.nextX
        y: self.nextY
        content: Rect {
            width: 40
            height: 40
			arcHeight: 10
			arcWidth: 10
            fill: orange
            stroke: black
        }
		transform: bind [translate(slideSelf.x,slideSelf.y), rotate(slideSelf.rotation,self.slideWidth/2,self.slideHeight/2)]
    }]

    prevSlide: bind self.getSlide(self.slideIndex - 1)
    currentSlide: bind self.getSlide(self.slideIndex)
    nextSlide: bind self.getSlide(self.slideIndex + 1)

    currentSlideX: bind self.currentSlide.x
    nextSlideX: bind self.nextSlide.x
    prevSlideX: bind self.prevSlide.x
    currentSlideRotation: bind self.currentSlide.rotation
};

operation SlideShow.getSlide( newSlideIndex:Number ) {
    if( (newSlideIndex >= sizeof (slides)) or (newSlideIndex < 0) ) {
        return null;
    } else {
        return slides[newSlideIndex];
    }
}

operation SlideShow.slideNext() {
    if( ((slideIndex + 1 ) == sizeof( slides )) or isAnimating) {
        return;
    }

    isAnimating = true;
    animatingNext = true;
    currentSlide.x = [currentSlide.x..-1-slideWidth] dur 1000 easein;
    nextSlide.x = [nextSlide.x..startX] dur 1000 easeout;
}

operation SlideShow.slidePrevious() {
    if( ((slideIndex - 1) < 0) or isAnimating ) {
        return;
    }

    isAnimating = true;
    animatingNext = false;
    currentSlide.x = [currentSlide.x..width + 1] dur 1000 easein;
    prevSlide.x = [prevSlide.x..startX] dur 1000 easeout;
}  

operation SlideShow.rotate() {
    if( isAnimating ) {
	return;
    }

    isAnimating = true;
    currentSlide.rotation = [0..720] dur 3000;
}

operation SlideShow.doNextAnimationStopped() {
    if( (currentSlide.x == -1-slideWidth) and (nextSlide.x == startX)) {
        slideIndex++;
        isAnimating = false;
    }
}

operation SlideShow.doPrevAnimationStopped() {
    if( (currentSlide.x == width + 1) and (prevSlide.x == startX)) {
        slideIndex--;
        isAnimating = false;
    }
}

trigger on SlideShow.currentSlideX = newValue {
    if( animatingNext == true ) {
        doNextAnimationStopped();
    } else {
        doPrevAnimationStopped();
    }
}

trigger on SlideShow.nextSlideX = newValue {
    doNextAnimationStopped();
}

trigger on SlideShow.prevSlideX = newValue {
    doPrevAnimationStopped();
}

trigger on SlideShow.currentSlideRotation = newValue {
    if( currentSlide.rotation == 720 ) {
	isAnimating = false;
    }
}

Frame {
    title: "Die, Ajax! - Slide Show Sample"
    width: 300
    height: 300
    visible: true
    content:
    Panel {
        content: [
        Canvas {
            var: self
            x: 0
            y: 0
            width: 300
            height: 300
            content:
            [slideShow.slides,
            View {
                transform: [translate(63,180)]
                content: GroupPanel {
                    cursor: DEFAULT
                    var row = Row {alignment: BASELINE}
                    var column1 = Column { }
                    var column2 = Column { }
                    var column3 = Column { }
                    rows: [row]
                    columns: [column1, column2, column3]
                    content:
                    [Button {
                        row: row
                        column: column1
                        text: "<-"
                        action: operation() {
                            slideShow.slideNext();
                        }
                    },Button {
                        row: row
                        column: column2
                        text: "Spin"
                        action: operation() {
                            slideShow.rotate();
                        }
                    }, Button {
                        row: row
                        column: column3
                        text: "->"
                        action: operation() {
                            slideShow.slidePrevious();
                        }
                    }]
                }
            }]
        }]
    }
}

And surely, you know how to do this part by now :) :

"C:\Program Files\Java\jre1.6.0_03\bin\java.exe" -classpath .;"C:\Documents and Settings\David Miles\My Documents\dev\OpenJFX\trunk\lib\javafxrt.jar";"C:\Documents and Settings\David Miles\My Documents\dev\OpenJFX\trunk\lib\swing-layout.jar";"C:\Documents and Settings\David Miles\My Documents\dev\OpenJFX\trunk\lib\Filters.jar" net.java.javafx.FXShell ShapeSlideShow.fx

Your paths may vary. Actually, I can just about guarantee they will. :)

Conclusions

Believe it or not, I actually condensed this code from what I’d initially written. I originally had a custom-coded triangle and a circle in the slideshow, but couldn’t get my triangle to rotate correctly and a rotating circle…not very exciting. Anyway, I hope this tutorial gave you a decent example of creating animations in JavaFX to plagiarize from. :) As the spec for JavaFX animation evolves, I will do more tutorials on this topic, but for now, get in there and start animating! And remember, you can always look to the JavaFX 2D Tutorial for more ideas and explanations.

Share and Enjoy:
These icons link to social bookmarking sites where readers can share and discover new web pages.
  • Digg
  • StumbleUpon
  • Reddit
  • del.icio.us
Leave a Donation

If you found this article helpful, please leave a donation for Dave, so he can help you again. As always, thank you for your support!

Related Posts:
Birds of a Feather: Silverlight, Flex and JavaFX
Silverlight Alpha 1.1 Refresh
10 Minute Tutorial - Silverlight: Using JavaScript to Call Scriptable Managed Code (C#)
10 Minute Tutorial - JavaFX: Hello World

Comments are closed.