Following on from the success of the JavaFX Devoxx animations Stephan Janssen asked if I could do a similar video to replace last years intro video that is played before the start of each of the recorded sessions. I decided this was another chance to see how JavaFX copes with animation. This time I needed a 10-15second clip of PAL video 768×576 25fps. In this blog I will explain how I created this animation and take you though recreating it step by step. I started out creating the graphics in Photoshop based on the ones I had from the Devoxx theme for Swish with all the parts that I am going to need to animate as separate layers. All the layers I want to access from JavaFX are named with the “jfx:” prefix and simple camel case names so they will be exposed to JavaFX code and come though as neat variable names.

Complete Graphics

Complete Graphics


You can download the final graphics photoshop file here, once you have this you will need Photoshop CS3 or CS4 and the JavaFX Production Suite (download here). If you don’t have Photoshop you can download a trial version to experiment with or use any design tool that will export SVG and the Production Suite convertor. Alternatively you could just save each layer as a PNG and hand code a scene graph with ImageViews for each layer. Once you have production suite installed in Photoshop you can go to “File” -> “Automate” -> ‘Save for JavaFX…” then save the graphics as “graphics.fxz” in the package directory in the source directory of a new Netbeans JavaFX project. You can then right click on the saved “fxz” file in Netbeans and choose “Generate UI Stub…” that will create a JavaFX “.fx” source file that is a scene graph Node class that you can use in your application. It will have variables for direct access to each of the layers you named in Photoshop with the “jfx:” prefix. Before you can compile the project with the newly generated stub you need to go to project properties and add the “JavaFX FXD 1.0” library to the project. Now all the code you need in Main to create a JavaFX application to display our graphics is this:

var graphics:graphicsUI;
Stage {
    title: "Devoxx Video Intro"
    scene: Scene {
        // PAL Video Sized
        width: 768 height: 576
        content: graphics = graphicsUI{}
        fill: Color.BLACK
    }
}

So run that and check it out, cool hey! So now its time to start animating it, lets start with the background. The background is made up of 3 layers so we can animate them.
 

Moody orange base background

Moody orange base background


Clouds layer

Clouds layer


Bar - grey is transparent

Bar - grey is transparent


The clouds layer is semi transparent and we are going to animate it slowly scrolling past to add mood it sits above the moody orange background and under the black bar. The graphics for it were created to match at the ends so it can be looped without you seeing the join. I created it by using the render clouds effect in photoshop, the result I duplicated and flipped about the middle so that both ends were the same and then hand blended the join. That way I knew it would repeat without a seam. In JavaFX I am going to make a second ImageView with the clouds as I need enough to scroll completely past the visible region then jump back and start again. Here is the code for this:

// Make copy of clouds image view so we have enough to scroll past
var cloudsGroup:Group = graphics.clouds_group as Group;
var clouds1:ImageView = graphics.clouds as ImageView;
clouds1.x = 0;
var clouds2:ImageView = ImageView {
    image: clouds1.image
    translateX: bind clouds1.image.width
}
insert clouds2 into cloudsGroup.content;
// == Animate clouds ===========================================================
TranslateTransition {
    node: cloudsGroup
    fromX: 0
    toX: -clouds1.image.width
    duration: 10s
    autoReverse: false
    repeatCount: Timeline.INDEFINITE
    interpolate: Interpolator.LINEAR
}.play();

Once I have created and positioned the second could imageview I create a TranslateTransition to animate the clouds scrolling past and start it playing. Next is the 3 spotlights:
 

Lights - 3 separate transparent layers

Lights - 3 separate transparent layers


The spotlights will animate rotating around the center of their top. To rotate around a point other than the center of a node we will have to use a Transform and Timeline as the RotateTransition only works around the center of the node.

// == Animate lights ===========================================================
var light1:ImageView = graphics.light1 as ImageView;
var light2:ImageView = graphics.light2 as ImageView;
var light3:ImageView = graphics.light3 as ImageView;
var light1angle = -20;
var light2angle = -20;
var light3angle = -20;
light1.transforms = Rotate { pivotX: light1.x + (light1.layoutBounds.width / 2) angle: bind light1angle };
light2.transforms = Rotate { pivotX: light2.x + (light2.layoutBounds.width / 2) angle: bind light2angle };
light3.transforms = Rotate { pivotX: light3.x + (light3.layoutBounds.width / 2) angle: bind light3angle };
var light1Timeline:Timeline = Timeline {
    keyFrames: [
         at (0s) {light1angle => -20 tween Interpolator.EASEBOTH}
         at (3s) {light1angle => 20 tween Interpolator.EASEBOTH}
     ]
    repeatCount: Timeline.INDEFINITE
    autoReverse: true
};
var light2Timeline:Timeline = Timeline {
    keyFrames: [
         at (0s) {light2angle => -20 tween Interpolator.EASEBOTH}
         at (3.5s) {light2angle => 20 tween Interpolator.EASEBOTH}
     ]
    repeatCount: Timeline.INDEFINITE
    autoReverse: true
};
var light3Timeline:Timeline = Timeline {
    keyFrames: [
         at (0s) {light3angle => -25 tween Interpolator.EASEBOTH}
         at (2.5s) {light3angle => 20 tween Interpolator.EASEBOTH}
     ]
    repeatCount: Timeline.INDEFINITE
    autoReverse: true
};

The first part of the code create 3 variables “light1angle”,”light2angle” and “light3angle” then setup Rotate Tranforms on the 3 light nodes whose angle is bound to these variables. We then create 3 timelines to animate the panning of the spotlights. Note: I have not started the animations here as I will do that in the final master timeline but you can add quick “light1Timeline.play();” style lines to test it out so far.  Next is the speaker chap(dude for americans):
 

Speak Chap - 2 layers head and body

Speak Chap - 2 layers head and body


The first animation for the speaker chap is to rotate his head back and forward. This is simple as in photoshop I manipulated the center point of the layer by adding a pixel at the right point to get the center where I needed it. Because of that we can just use a simple RotateTransition:

// == Chap Head Rotation ================================================================
var headRotation:RotateTransition = RotateTransition {
    fromAngle: -5
    toAngle: 5
    autoReverse: true
    duration: 1s
    interpolate: Interpolator.EASEBOTH
    repeatCount: Timeline.INDEFINITE
    node: graphics.head
};

Again I have not started this transition as it will be started at the right point in the master timeline. Next I will setup a clip on the group containing the chap’s head and body and link it to a variable that controls the beam position. This is so I can BEAM him in 🙂 for a bit of fun.
 

Beam layer

Beam layer


The “scanLocation” variable can be from 0.0 fully invisible to 1.0 fully visible. We will animate this later to beam the chap in.

// == Chap scan ================================================================
var chap:Group = graphics.chap as Group;
var chapX:Number = chap.layoutBounds.minX;
var chapY:Number = chap.layoutBounds.minY;
var chapWidth:Number = chap.layoutBounds.width;
var chapHeight:Number = chap.layoutBounds.height;
var beam:ImageView = graphics.beam as ImageView;
var scanLocation:Number = 0.7 on replace {
    beam.y = chapY+(chapHeight*scanLocation) - (beam.layoutBounds.height/2);
};
chap.clip = Rectangle {
    x: chapX
    translateY: bind chapY-chapHeight+(chapHeight*scanLocation)
    width: chapWidth
    height: chapHeight
};

Next is the title text, this is made in 3 layers:
 

Base text including small glow

Base text including small glow


Larger glow for text

Larger glow for text


Mask for large glow

Mask for large glow


We will animate the glow mask across the text exposing a small part of the larger glow as we go, this will give the effect of a glow sweeping across the text. In the code for this I setup the mask image as a clip on the text glow layer and calculate the start and end positions for the animation.

// == Glow Mask ================================================================
var glow:ImageView = graphics.glow as ImageView;
var glowMaskGraphics:ImageView = graphics.glowMask as ImageView;
glowMaskGraphics.visible = false;
var glowX:Number = glow.layoutBounds.minX;
var glowWidth:Number = glow.layoutBounds.width;
var glowMaskWidth:Number = glowMaskGraphics.layoutBounds.width;
var glowMaskStart:Number = glowX - glowMaskWidth;
var glowMaskEnd:Number = glowX + glowWidth + glowMaskWidth;
var glowMaskPosition:Number = glowMaskStart;
var glowMask:ImageView = ImageView {
    image: glowMaskGraphics.image
    x: bind glowMaskPosition
    y: glowMaskGraphics.y
};
glow.clip = glowMask;

We have a music recording from Devoxx of the beatboxer Roxorloops that we are going to use for the sound track for this intro so next we need to setup the media ready to be played.

// == Setup Music ==============================================================
var music:MediaPlayer = MediaPlayer {
    media: Media { source: "https://jasperpotts.com/blogfiles/devox-intro-video/IntroBeat.mp3" }
    autoPlay: true
}
music.stop();

That is all the setup done for the little animations, now we need to get started on the master timeline which is going to control the sequence of all the animations. First create a bunch of constants for all the times of the key points in the animation. This is handy as I can play with the timing to get it feeling right and in sync with the sound without having to dig too deeply into the code.

// == Main Animation Times =====================================================
def FRAME:Duration = 1s/60;
def START:Duration = 5s;
def FIRST_LIGHT:Duration = START + 0.2s;
def SECOND_LIGHT:Duration = FIRST_LIGHT + 3s;
def THIRD_LIGHT:Duration = SECOND_LIGHT + 1s;
def START_SHOW_CHAP:Duration = THIRD_LIGHT + 1s;
def END_SHOW_CHAP:Duration = START_SHOW_CHAP + 1s;
def START_SHOW_TEXT:Duration = END_SHOW_CHAP + 1s;
def END_SHOW_TEXT:Duration = START_SHOW_TEXT + 1s;
def START_GLOW_SCAN:Duration = END_SHOW_TEXT + 0.5s;
def END_GLOW_SCAN:Duration = START_GLOW_SCAN + 5s;
def START_HIDE:Duration = END_GLOW_SCAN + 0.5s;
def END_HIDE:Duration = START_HIDE + 1.5s;

Then the final part is the main timeline that fades layers in and out and starts the smaller animations at the right points. This is the last of the code and you have the completed application.

// == Main Animation ===========================================================
Timeline {
    keyFrames: [
        KeyFrame {
            time: 0s
            values: [
                beam.visible => false
                graphics.bar.visible => false
                graphics.chap.visible => false
                graphics.background.opacity => 0
                graphics.flames.opacity => 0
                graphics.light1.opacity => 0
                graphics.light2.opacity => 0
                graphics.light3.opacity => 0
                graphics.text.opacity => 0
            ]
            action: function() { light1Timeline.play(); }
        }
        KeyFrame { // start second light a little later as we don't want them in sync
            time: 1s
            action: function() { light2Timeline.play(); }
        }
        KeyFrame { // start music
            time: START - 0.1s
            action: function() {
                music.currentTime = 0s;
                music.play();
            }
        }
        KeyFrame { // start after couple seconds in as a chance to stablize after launch
            time: START
            values: [
                graphics.background.opacity => 0
                graphics.flames.opacity => 0
                graphics.light1.opacity => 0
                graphics.light2.opacity => 0
                graphics.light3.opacity => 0
                graphics.bar.visible => true
            ]
            action: function() { light3Timeline.play(); }
        }
        KeyFrame {
            time: FIRST_LIGHT
            values: [
                graphics.light3.opacity => 0.6 tween Interpolator.LINEAR
                graphics.background.opacity => 0.1 tween Interpolator.EASEIN
                graphics.flames.opacity => 0.1 tween Interpolator.EASEIN
            ]
        }
        KeyFrame {
            time: SECOND_LIGHT - 0.2s
            values: graphics.light1.opacity => 0.0
        }
        KeyFrame {
            time: SECOND_LIGHT
            values: [
                graphics.light1.opacity => 0.6 tween Interpolator.LINEAR
                graphics.background.opacity => 0.6 tween Interpolator.EASEIN
                graphics.flames.opacity => 0.6 tween Interpolator.EASEIN
            ]
        }
        KeyFrame {
            time: THIRD_LIGHT - 0.2s
            values: graphics.light2.opacity => 0.0
        }
        KeyFrame {
            time: THIRD_LIGHT
            values: [
                graphics.light2.opacity => 0.6 tween Interpolator.LINEAR
                graphics.background.opacity => 1.0 tween Interpolator.EASEIN
                graphics.flames.opacity => 1.0 tween Interpolator.EASEIN
            ]
        }
        KeyFrame {
            time: START_SHOW_CHAP - 0.2s
            values: [
                beam.opacity => 0.0
                beam.visible => true
            ]
        }
        KeyFrame {
            time: START_SHOW_CHAP
            values: [
                graphics.chap.visible => true
                beam.opacity => 1.0 tween Interpolator.EASEIN
                scanLocation => 0.0
            ]
        }
        KeyFrame {
            time: END_SHOW_CHAP
            values: [
                beam.visible => false
                scanLocation => 1.0 tween Interpolator.EASEIN
            ]
            action: function() { headRotation.play(); }
        }
        KeyFrame {
            time: START_SHOW_TEXT
            values: [
                graphics.text.opacity => 0.0
            ]
        }
        KeyFrame {
            time: END_SHOW_TEXT
            values: [
                graphics.text.opacity => 1.0
                graphics.glow.visible => true
            ]
        }
        KeyFrame {
            time: START_GLOW_SCAN
            values: [
                glowMaskPosition => glowMaskStart
            ]
        }
        KeyFrame {
            time: END_GLOW_SCAN
            values: [
                glowMaskPosition => glowMaskEnd tween Interpolator.EASEBOTH
            ]
        }
        KeyFrame {
            time: START_HIDE
            values: [
                graphics.opacity => 1.0 tween Interpolator.EASEOUT
            ]
        }
        KeyFrame {
            time: END_HIDE
            values: [
                graphics.opacity => 0.0 tween Interpolator.EASEOUT
            ]
        }
        KeyFrame {
            time: END_HIDE + 3s
            action: function () { FX.exit(); }
        }
    ]
}.play();

So we are finally there, if you have been following along then you can run what you have for the rest of you here is a web start link:

You can also download the Netbeans project complete with FXZ graphics file: Netbeans Project. The first session with the new intro video is already live on Parleys.com watch it here “Inside the SpringSorce Application Platform”. Well I hope you enjoyed my longest blog so far and a cool little application.
Update: Some people have been having problems with a media bug playing the sound, I am passing it on to the Media Team in the mean time here is a webstart link without music: