I had the pleasure of addressing the Chicago Java Users Group and jamming with JUG leader Freddy Guime in the process. The presentation was a gentle introduction to features in Java 8 such as Lambdas, the Streams API, the new Date/Time API, new JavaFX features, and Java Mobile Edition 8. To demonstrate multi-touch and 3D features, I played some blues on the ZenGuitar JavaFX app while Freddy riffed on his real guitar. Please watch the video below to learn about features of Java 8, launched in March 2014.
You can view the slides from the presentation below:
One of the very interesting and useful platforms for JavaFX is touch enabled devices such as Windows 8 tablets. This session at JayDay/Munich on 01-Jul-2013 will demonstrate a variety of JavaFX applications running on a Surface Windows 8 Pro, pointing out capabilities in JavaFX designed to leverage touch capabilities.
The slides presented at this session are shown in the SlideShare presentation below. The code for the demos presented in the session is on GitHub. I hope to see you at JayDay!
During the JavaFX keynote at OSCON/Java a few days ago I demonstrated the the EarthCubeFX application ported to JavaFX 2.0. In the session I promised to make the code available on my blog, so here is a link from which you can download the EarthCubeX project (for NetBeans IDE). Some helpful tips:
After downloading the ZIP file, expand the project into a directory of your choice.
Start NetBeans, and select the File -> Open Project menu.From the Open Project dialog box,
Navigate to your chosen directory and open the EarthCubeX project. If you receive a message dialog stating that the jfxrt.jar file can't be found, click the Resolve button and navigate to the rt/lib folder subordinate to where you installed the JavaFX 2.0 SDK.
Note: You can obtain the NetBeans IDE from the NetBeans site.
Also, although JavaFX 2.0 for Mac OS will be released after JavaFX 2.0 for Windows is released, it doesn't seem to be far behind, as this video of EarthCube on an early access version of JavaFX for Mac OS demonstrates.
If you've followed this blog, you know that I'm passionate about rich internet application (RIA) development, particularly rich-client Java development (i.e. the client runs on the JVM, is visually pleasing, intuitive and responsive).
Readers of this blog also know that I view JavaFX and Java SE 6u10 as the principal pieces of the RIA puzzle that will bring rich-client Java back [we almost had it in 1996... I'll spare you the rant just this once :-)]. With JavaOne around the corner, it should come as no surprise that I'll be presenting again on JavaFX, joining the ranks of others passionate about rich-client Java.
During my presentations, I'll announce the winner of the RIA Exemplar Challenge as well. I hope to see you there, and please do come up and say hi after the presentations!
The previous blog post provided an example, named EarthCubeFX, of creating a 3D application with JavaFX 1.3.
Today's post highlights Oracle JavaFX engineer and author Jim Clarke's experience running EarthCubeFX on JavaFX-TV. To quote Mr. Clarke:
The tag line is "JavaFX: Bringing Rich Experiences to All the Screens of Your Life", so I decided to put it to the test. Jim Weaver created a 3D demo called EarthCubeFX, and I wanted to know what needed to be done for it to run on a true JavaFX-TV platform. Luckily, I had just obtained an Intel Canmore CE3100 system that runs JavaFX-TV.
Obviously, the TV does not have a mouse, so the first thing I had to do is use some of the keys on the remote control to invoke the same behavior that the mouse buttons activated in Jim's original code. The JavaFX-TV runtime maps the remote control keys to JavaFX KeyEvent objects. The first choice was to decide which keys to use for the pitch and roll rotations; the remote's arrow keys were the obvious choice. I arbitrarily picked the CHANNEL_UP and CHANNEL_DOWN keys to do the Z translation, which moves the cube further back or forward in the field of view. Lastly, I used the OK key to take the cube back to its home position. I only added 8 lines of code, compiled the application in NetBeans using the JavaFX-TV Emulator, then copied the Jar file over to the Canmore and ran it as shown in the video. The Google map tile images are still fetched over the Internet. The animation, and 3D features, run exactly as on the desktop.
Here's a short video that Jim Clarke created of EarthCubeFX running in JavaFX-TV on the Canmore set-top box:
Also, here is the code that Jim Clarke added to handle remote controller keys:
public function keyRotation(ke: KeyEvent) : Void {
if (ke.code == KeyCode.VK_RIGHT) {
angleY++;
}
else if (ke.code == KeyCode.VK_LEFT) {
angleY--;
}
else if (ke.code == KeyCode.VK_UP) {
angleX--;
}
else if (ke.code == KeyCode.VK_DOWN) {
angleX++
}
else if (ke.code == KeyCode.VK_CHANNEL_UP) {
translateZ -= 10;
}
else if (ke.code == KeyCode.VK_CHANNEL_DOWN) {
translateZ += 10;
}
else if (ke.code == KeyCode.VK_ENTER) {
goHomePosition();
}
}
Thanks, Mr. Clarke! Also, remember that the deadline for RIA Exemplar Challenge entries is 22-May-2010 so please submit it if you haven't already!
As mentioned in the Your Calendar PWN3D post, the JavaFX 1.3 SDK, released 22-Apr-2010, contains some basic 3D-related features with which you can begin experimenting:
There is a new package, named javafx.runtime, which contains classes that allow an application to query the capabilities of the platform. One of these capabilities is represented by the SCENE3D constant of the ConditionalFeature class.
The Point3D class in the javafx.geometry package represents a point in 3D space, with variables named x, y and z.
The Node class contains scaleZ and translateZ variables, for scaling and translating (moving) a node on the Z-axis.
The Node class also contains a rotationAxis variable, which defines the rotation axis (X, Y or Z) on which a node will rotate at the angle specified in that node's rotate variable.
The Rotate and Scale classes contained in the javafx.scene.transform package have instance variables named pivotX, pivotY and pivotZ that define a rotation pivot point.
The RotationTransition, ScaleTransition, and TranslateTransition classes accept a three dimensional axis.
The PerspectiveCamera class, located in the javafx.scene package, defines a viewing volume (which is shaped like a truncated right pyramid) for a perspective projection. Its fieldOfView variable specifies the vertical angle of the camera's projection.
In this post I’d like to introduce you to these features in the context of the EarthCubeFX example.
The EarthCubeFX Example Program
The EarthCubeFX example program features a cube that contains Google Map tiles on its faces. You can download the NetBeans project containing this example here. After downloading and unzipping the file, open the project in NetBeans. To enable the stack that implements 3D capabilities, the NetBeans project contains the following JVM argument in the Run pane of its Project Properties dialog:
-Xtoolkit prism
Because the program access Google Map tiles from the Internet at runtime, verify that you have an Internet connection when running the program. Here are some instructions for using the EarthCubeFX example:
Taking EarthCubeFX for a Spin
When the EarthCubeFX application is invoked, it appears in a transparent (and undecorated) window after having requested image tiles via the Internet from Google Maps:
To make the appearance more interesting than a blank cube, the Google Map image tiles from a round planet are placed on the cube. In keeping with the Earth theme, the cube orients itself on the Y axis as if the Sun were behind the user, and then slowly turns to continue tracking the Sun. At the time the screen shot above was taken, the Sun was warming the Atlantic Ocean. The user may interact with the cube in the following ways:
Dragging the face of the cube causes it to rotate in the direction of the drag.
Pressing the spacebar key fades the Google Map image tiles, exposing the plain cube as shown in the screen shot below. Note that in this mode, the cube doesn’t track the Sun. Pressing the spacebar again causes the map tiles to fade in, and the tracking behavior to resume.
Dragging the face of the cube while holding the control key down moves the cube around the screen.
Dragging the face of the cube in the up or down direction while holding the alt key down causes it to move on the Z axis, toward or away from the user, respectively.
Double-clicking the face of the cube causes it to rotate to its original X and Y axis angles.
Pressing the esc key closes the EarthCubeFX application.
Here’s a short video of these interactions with the cube:
Examining the EarthCubeFX Code
Now that you are familiar with the behavior of EarthCubeFX, let’s examine its 3D related code together. First, here’s the code for the CubeNode itself:
/*
* CubeNode.fx
*
* A cube-shaped UI component upon whose faces other nodes may placed.
*
* Developed by James L. Weaver (jim.weaver#javafxpert.com) to demonstrate the
* use of SCENE3D conditional features in the JavaFX 1.3 API
*/
package javafxpert.cube;
import javafx.animation.*;
import javafx.scene.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.scene.shape.*;
import javafx.scene.transform.*;
import javafx.util.Math.*;
import javafx.util.Sequences;
def HOME_ANGLE_X = 20.0;
def HOME_ANGLE_Y = -30.0;
def MIN_ANGLE_X = -70.0;
def MAX_ANGLE_X = 70.0;
def MIN_ANGLE_Y = -720.0;
def MAX_ANGLE_Y = 720.0;
def MIN_TRANSLATE_Z = 0.0;
def MAX_TRANSLATE_Z = 10000.0;
public class CubeNode extends CustomNode {
postinit {
goHomePosition();
}
var r:Number;
public var edgeLength:Number = 100 on replace {
r = edgeLength * 0.5;
};
public-init var frontNode:Node;
public-init var rearNode:Node;
public-init var leftNode:Node;
public-init var rightNode:Node;
public-init var topNode:Node;
public-init var bottomNode:Node;
public var faceHue:Number = 211;
public var faceSat:Number = 0.25;
override var translateZ on replace {
if (translateZ > MAX_TRANSLATE_Z) {
translateZ = MAX_TRANSLATE_Z;
}
else if (translateZ < MIN_TRANSLATE_Z) {
translateZ = MIN_TRANSLATE_Z;
}
}
var angleX:Number on replace {
if (angleX > MAX_ANGLE_X) {
angleX = MAX_ANGLE_X;
}
else if (angleX < MIN_ANGLE_X) {
angleX = MIN_ANGLE_X;
};
arrangeFacesZOrder();
};
var angleY:Number on replace {
if (angleY > MAX_ANGLE_Y) {
angleY = MAX_ANGLE_Y;
}
else if (angleY < MIN_ANGLE_Y) {
angleY = MIN_ANGLE_Y;
}
arrangeFacesZOrder();
};
var dragPressedAngleX:Number;
var dragPressedAngleY:Number;
var dragStartOffsetX:Number;
var dragStartOffsetY:Number;
var rearFace:CubeFace = CubeFace {
transforms: [
Translate {
x: 0
y: 0
z: edgeLength
},
Rotate {
angle: 180.0
axis: Rotate.Y_AXIS
pivotX: edgeLength / 2
}
]
node: Group {
content: bind [
createFaceRectangle(rearFace),
rearNode
]
}
};
var bottomFace:CubeFace = CubeFace {
transforms: [
Translate {
x: 0
y: 0
z: edgeLength
},
Rotate {
angle: 90.0
axis: Rotate.X_AXIS
pivotY: edgeLength
}
]
node: Group {
content: bind [
createFaceRectangle(bottomFace),
bottomNode
]
}
};
var leftFace:CubeFace = CubeFace {
transforms: [
Translate {
x: 0
y: 0
z: edgeLength
},
Rotate {
angle: 90.0
axis: Rotate.Y_AXIS
pivotX: 0
}
]
node: Group {
content: bind [
createFaceRectangle(leftFace),
leftNode
]
}
};
var rightFace:CubeFace = CubeFace {
transforms: [
Translate {
x: 0
y: 0
z: edgeLength
},
Rotate {
angle: -90.0
axis: Rotate.Y_AXIS
pivotX: edgeLength
}
]
node: Group {
content: bind [
createFaceRectangle(rightFace),
rightNode
]
}
};
var topFace:CubeFace = CubeFace {
transforms: [
Translate {
x: 0
y: 0
z: edgeLength
},
Rotate {
angle: -90.0
axis: Rotate.X_AXIS
pivotY: 0
}
]
node: Group {
content: bind [
createFaceRectangle(topFace),
topNode
]
}
};
var frontFace:CubeFace = CubeFace {
transforms: [
Translate {
x: 0
y: 0
z: 0
}
]
node: Stack {
content: bind [
createFaceRectangle(frontFace),
frontNode
]
}
};
function arrangeFacesZOrder():Void {
var baseGroup = children[0] as Group;
rearFace.zPos = r * cos(toRadians(angleY + 0));
bottomFace.zPos = r * cos(toRadians(angleX + 270));
leftFace.zPos = r * cos(toRadians(angleY + 270));
rightFace.zPos = r * cos(toRadians(angleY + 90));
topFace.zPos = r * cos(toRadians(angleX + 90));
frontFace.zPos = r * cos(toRadians(angleY + 180));
baseGroup.content = (Sequences.sort(baseGroup.content as CubeFace[]) as Node[]);
}
function createFaceRectangle(face:CubeFace):Rectangle {
Rectangle {
width: edgeLength
height: edgeLength
fill: bind computeFaceHSB(faceHue, faceSat, face.zPos, r)
}
}
bound function computeFaceHSB(faceHue:Number, faceSat:Number,
zPos:Number, radius:Number):Color {
Color.hsb(faceHue, faceSat, abs(-zPos / (radius * 2)) + 0.40)
}
public function goHomePosition():Void {
var homeTimeline = Timeline {
keyFrames: [
KeyFrame {
time: 1000ms
values: [
angleX => HOME_ANGLE_X tween Interpolator.EASEBOTH,
angleY => HOME_ANGLE_Y tween Interpolator.EASEBOTH,
]
}
]
};
homeTimeline.play();
}
public function goPosition(angX:Number, angY:Number):Void {
var tempAngX = angX;
var tempAngY = angY;
if (tempAngX.isNaN()) {
tempAngX = HOME_ANGLE_X;
}
if (tempAngY.isNaN()) {
tempAngY = HOME_ANGLE_Y;
}
var goTimeline = Timeline {
keyFrames: [
KeyFrame {
time: 1000ms
values: [
angleX => tempAngX tween Interpolator.EASEBOTH,
angleY => tempAngY tween Interpolator.EASEBOTH
]
}
]
};
goTimeline.play();
}
override var children = bind [
Group {
content: [
rearFace,
bottomFace,
leftFace,
rightFace,
topFace,
frontFace
]
transforms: [
Rotate {
angle: bind angleX
axis: Rotate.X_AXIS
pivotX: edgeLength * 0.5
pivotY: edgeLength * 0.5
pivotZ: edgeLength * 0.5
},
Rotate {
angle: bind angleY
axis: Rotate.Y_AXIS
pivotX: edgeLength * 0.5
pivotY: edgeLength * 0.5
pivotZ: edgeLength * 0.5
}
]
onMousePressed: function(me:MouseEvent):Void {
dragPressedAngleX = angleX;
dragPressedAngleY = angleY;
dragStartOffsetX = me.screenX - scene.stage.x;
dragStartOffsetY = me.screenY - scene.stage.y;
}
onMouseDragged: function(me:MouseEvent):Void {
if (me.controlDown) {
scene.stage.x = me.screenX - dragStartOffsetX;
scene.stage.y = me.screenY - dragStartOffsetY;
}
else if (me.altDown) {
translateZ = translateZ + me.dragY * 10;
}
else {
angleX = (me.dragY / 2) + dragPressedAngleX;
angleY = (me.dragX / -2) + dragPressedAngleY;
}
}
onMouseClicked: function(me:MouseEvent):Void {
if (me.clickCount == 2) {
goHomePosition();
}
}
}
]
}
The first concept from the code that we’ll examine is the ability in JavaFX 1.3 to translate (move) nodes on the Z axis.
Translate Transformations on the Z Axis
There are three ways in JavaFX to perform a translate transformation on the Z axis. The first way is to use the javafx.scene.transform.Translate class. Another way involves the use of the translateZ instance variable of the node that you wish to translate. The third way is to use the TranslateTransition class, located in the javafx.animation.transition package. Each of these ways produces the same result: moving a node toward or away from the user.
Using the Translate Class
The code snippet below from the CubeNode.fx listing above demonstrates the use of the Translate class:
When the CubeNode is instantiated, six CubeFace instances are created and placed in the scene graph. The snippet above shows one of these instances (the rear face) being created and positioned at distance equal to the value of edgeLength (the length of the cube edges) on the Z axis of the cube. The Translate instance is used in the transforms sequence of the Node class, whose purpose is to express one or more transformations. In this case, the Translate is performed, followed by the Rotate, which will be discussed shortly.
Using the Node translateZ Variable
The following code snippet from CubeNode.fx demonstrates the use of the translateZ variable:
When the user drags the mouse while holding the alt key down, the translateZ variable of CubeNode (that it inherits from Node) is increased or decreased as the mouse is dragged up or down. Increasing the translateZ variable, for example, moves the cube away from the user, making it appear smaller.
The TranslateTransition Class
Another way to perform a translate transformation is to use the TranslateTransition class, located with other transition-related classes in the javafx.animation.transition package. See the JavaFX 1.3 API documentation for the TranslateTransition class for details.
Rotate Transformations
As with translate transformations, there are three ways in JavaFX to perform rotate transformations. One way is to use the javafx.scene.transform.Rotate class. Another way involves the use of the rotate and rotationAxis instance variables of the node that you wish to rotate. The third way is to use the RotateTransition class, located in the javafx.animation.transition package. Each of these ways produces the same result: rotating a node on a given axis.
Using the Rotate Class
The code snippet below from the CubeNode.fx listing above demonstrates the use of the Rotate class:
The snippet above shows the left CubeFace being instantiated and positioned at edgeLength on the Z axis of the cube, which as you may recall is where the rear face was placed. It is then rotated 90 degrees on the Y axis (clockwise as looking from above). Because the pivotX variable is assigned a value of 0, the rotation occurs around the leftmost point on the leftFace node. As with the Translate instance shown earlier, the Rotate instance is used in the transforms sequence of the Node class, whose purpose is to express one or more transformations.
The Node rotate and rotationAxis Variables
An alternative to using the Rotate class is to simply use the rotate and rotationAxis variables of the Node class. You can specify the angle of rotation and the axis of rotation, but this technique doesn’t allow defining multiple axes of rotation nor does it allow identifying a pivot point around which the rotation is to occur.
The RotateTransition Class
Another way to perform a rotate transformation is to use the RotateTransition class, located with other transition-related classes in the javafx.animation.transition package. See the JavaFX 1.3 API documentation for the RotateTransition class for details.
Scale Transformations
As with translate and rotate transformations, there are three ways in JavaFX to perform scale transformations. One way is to use the javafx.scene.transform.Scale class, which allows you to specify pivot points about which the scales are to occur. Another way involves the use of the scaleX, scaleY, and scaleZ variables of the node that you wish to scale. The third way is to use the ScaleTransition class, located in the javafx.animation.transition package.
Some Other 3D-Related Features in JavaFX 1.3
There are a couple more 3D capabilities of JavaFX 1.3 that I’d like to point out. First, however, here is the source code for EarthCubeMain.fx, the main script in this example:
/*
* EarthCubeMain.fx
*
* Main script for EarthCubeFX -- a program that uses the CubeNode component
* and superimposes map tiles from Google Maps web services.
*
* Developed by James L. Weaver (jim.weaver#javafxpert.com) to demonstrate the
* use of SCENE3D conditional features in the JavaFX 1.3 API
*/
package javafxpert.earthcube;
import javafx.animation.*;
import javafx.runtime.*;
import javafx.scene.*;
import javafx.scene.effect.PerspectiveTransform;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.stage.*;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import javafxpert.cube.CubeNode;
if (not Platform.isSupported(ConditionalFeature.SCENE3D)) {
println("The SCENE3D conditional feature is not supported");
FX.exit();
}
def EDGE_LENGTH:Number = 512;
var cube:CubeNode;
var mapOpacity:Number = 0.0;
def showMapTimeline = Timeline {
keyFrames: [
KeyFrame {
time: 0s
values: mapOpacity => 0.0
action: function():Void {
cube.goPosition(Number.NaN, calculateAngleYForCurrentTime());
trackWithCurrentTime.playFromStart();
}
},
at(1s) {mapOpacity => 0.7 tween Interpolator.EASEBOTH;}
]
}
def hideMapTimeline = Timeline {
keyFrames: [
KeyFrame {
time: 0s
values: mapOpacity => 0.7
action: function():Void {
trackWithCurrentTime.stop();
}
},
at(1s) {mapOpacity => 0.0 tween Interpolator.EASEBOTH;}
]
}
def trackWithCurrentTime = Timeline {
keyFrames: [
KeyFrame {
time: 10m
action: function():Void {
cube.goPosition(Number.NaN, calculateAngleYForCurrentTime());
}
}
]
repeatCount: Timeline.INDEFINITE
};
function calculateAngleYForCurrentTime():Number {
def cal = new GregorianCalendar();
cal.setTimeZone(TimeZone.getTimeZone("GMT"));
def hour:Number = cal.get(Calendar.HOUR_OF_DAY);
def minute:Number = cal.get(Calendar.MINUTE);
def angY:Number = (((hour / 24.0) + (minute / 1440.0)) * 360) mod 360;
return angY;
}
function createSideMapTiles(sideNum:Integer):Node {
def xOffset = sideNum * 2;
Tile {
columns: 2
rows: 2
content: for (y in [3..4], x in [xOffset..xOffset + 1]) {
ImageView {
var xm = (x + 1) mod 8
opacity: bind mapOpacity
image: Image {
url: "http://mt3.google.com/vt/v=w2.97&x={xm}&y={y}&z=3"
}
fitWidth: EDGE_LENGTH / 2
fitHeight: EDGE_LENGTH / 2
}
}
}
}
function createTopMapTiles(sideNum:Integer):Node {
def xOffset = sideNum * 2;
Tile {
rotate: (450 - sideNum * 90) mod 360
columns: 2
rows: 3
content: for (y in [0..2], x in [xOffset..xOffset + 1]) {
ImageView {
var xm = (x + 1) mod 8
opacity: bind mapOpacity
image: Image {
url: "http://mt3.google.com/vt/v=w2.97&x={xm}&y={y}&z=3"
}
fitWidth: EDGE_LENGTH / 2
fitHeight: EDGE_LENGTH / 3
}
}
effect: PerspectiveTransform {
ulx: EDGE_LENGTH * 0.375 uly: EDGE_LENGTH * 0.625
urx: EDGE_LENGTH * 0.625 ury: EDGE_LENGTH * 0.625
llx: 0 lly: EDGE_LENGTH
lrx: EDGE_LENGTH lry: EDGE_LENGTH
}
}
}
function createBottomMapTiles(sideNum:Integer):Node {
def xOffset = (sideNum + 1) * 2;
Tile {
rotate: (sideNum * 90) mod 360
columns: 2
rows: 3
content: for (y in [5..7], x in [xOffset..xOffset + 1]) {
ImageView {
var xm = (x + 1) mod 8
opacity: bind mapOpacity
image: Image {
url: "http://mt3.google.com/vt/v=w2.97&x={xm}&y={y}&z=3"
}
fitWidth: EDGE_LENGTH / 2
fitHeight: EDGE_LENGTH / 3
}
}
effect: PerspectiveTransform {
ulx: 0 uly: 0
urx: EDGE_LENGTH ury: 0
llx: EDGE_LENGTH * 0.375 lly: EDGE_LENGTH * 0.375
lrx: EDGE_LENGTH * 0.625 lry: EDGE_LENGTH * 0.375
}
}
}
var stageRef:Stage;
stageRef = Stage {
style: StageStyle.TRANSPARENT
scene: Scene {
fill: Color.TRANSPARENT
width: 800
height: 800
camera: PerspectiveCamera {
fieldOfView: 30
}
content: [
cube = CubeNode {
layoutX: 130
layoutY: 130
focusTraversable: true
onKeyPressed: function(ke:KeyEvent):Void {
if (ke.code == KeyCode.VK_SPACE) {
if (mapOpacity == 0.0) {
showMapTimeline.playFromStart();
}
else {
hideMapTimeline.playFromStart();
}
}
else if (ke.code == KeyCode.VK_ESCAPE) {
stageRef.close();
}
}
edgeLength: EDGE_LENGTH
leftNode: createSideMapTiles(0)
frontNode: createSideMapTiles(1)
rightNode: createSideMapTiles(2)
rearNode: createSideMapTiles(3)
topNode: Stack {
content: [
for (side in [0..3]) createTopMapTiles(side),
ImageView {
opacity: bind mapOpacity
image: Image {
url: "http://mt3.google.com/vt/v=w2.97&x=2&y=8&z=4"
}
fitWidth: EDGE_LENGTH / 4
fitHeight: EDGE_LENGTH / 4
}
]
}
bottomNode: Stack {
content: [
for (side in [0..3]) createBottomMapTiles(side),
ImageView {
opacity: bind mapOpacity
image: Image {
url: "http://mt3.google.com/vt/v=w2.97&x=2&y=15&z=4"
}
fitWidth: EDGE_LENGTH / 4
fitHeight: EDGE_LENGTH / 4
}
]
}
}
]
}
}
showMapTimeline.playFromStart();
Using the Scene PerspectiveCamera Class
Much of the code in the main script file shown above creates the nodes containing Google Map tiles that appear on the faces of the cube. Some of the code also causes the cube to rotate on the Y-axis periodically in order to track the position of the Sun, and handles keyboard input.
Some 3D related code that deserves more explanation is in the following snippet:
The PerspectiveCamera causes the scene to be rendered using a perspective projection, and its fieldOfView variable specifies the vertical angle of the camera’s projection.
Querying the Capabilities of the Platform at Runtime
There is a new package in JavaFX 1.3 named javafx.runtime that enables your program to query the runtime platform’s capabilities. Here’s a snippet from the EarthCubeMain.fx listing that demonstrates how to check for the platform’s support for a 3D scene graph:
if (not Platform.isSupported(ConditionalFeature.SCENE3D)) {
println("The SCENE3D conditional feature is not supported");
FX.exit();
}
Conclusion
For completeness, here is the listing for the third file in the EarthCubeFX example, which defines a cube face on a CubeNode:
/*
* CubeFace.fx
*
* Represents a face on the CubeNode UI component.
*
* Developed by James L. Weaver (jim.weaver#javafxpert.com) to demonstrate the
* use of SCENE3D conditional features in the JavaFX 1.3 API
*/
package javafxpert.cube;
import javafx.scene.*;
import java.lang.Comparable;
import java.lang.Object;
public class CubeFace extends CustomNode, Comparable {
package var node:Node;
package var zPos:Number;
override var children = bind node;
override public function compareTo(cubeFace:Object) {
return (cubeFace as CubeFace).zPos.compareTo(zPos)
}
}
As you may have noticed in the arrangeFacesZOrder() function of the CubeNode.fx listing earlier in the article, when the cube is rotated the Z order of the faces must be manipulated programmatically so that a cube face doesn’t obscure another cube face that is in front of it. In this example, the following statement located in the arrangeFacesZOrder() function is executed to sort the CubeFace instances within their Group:
baseGroup.content = (Sequences.sort(baseGroup.content as CubeFace[]) as Node[]);
To support being sorted with the Sequences.sort() function, the CubeFace class extends the java.util.Comparable interface.
Future versions of JavaFX will have 3D shapes such as cubes, and will support other 3D constructs such as texture mapping and lighting. The JavaFX 1.3 SDK, however, provides a great opportunity to experiment with some basic 3D capabilities now. Please leave a comment if you have a question about this EarthCubeFX example.
In an earlier post, I featured the "JavaFX meets Java 3D" work that August Lammersdorf of InteractiveMesh.org is doing. In a nutshell, he is developing infrastructure that connects Java 3D and JavaFX, in which the 3D canvas is rendered in a lightweight component. This infrastructure makes it easy for developers to create JavaFX applications that contain a Java 3D universe. He has taken this work to a new level, as demonstrated by an animated 3D piston and propeller in a virtual universe being rendered on puzzle pieces in JavaFX.
The controls at the bottom of the UI are JavaFX components that control the Java 3D universe. For example, the Rotation slider controls the animation of the 3D piston/propeller. The Shuffle button breaks the propeller (whilst spinning if you please) into puzzle pieces as shown below:
You can then drag the puzzle pieces to their correct locations and solve the puzzle. Here's an exceprt of this program's description from the InteractiveMesh.org website, where you can play with this puzzle:
"The Java 3D scene of an animated propeller engine is rendered into several puzzle pieces. The puzzle board is resizeable from 2x2 up to 8x8 pieces on a screen of 1200 pixels height. This application is using source code of the JavaFX sample 'PuzzlePieces'. The engine animation, the viewpoint setting, and the 3D mouse navigation (on the puzzle board) are enabled even if the puzzle pieces are shuffled."
Thanks August, and please know that I'm using "propeller-head" in a most respectful manner :-)
Occasionally the requirement arises to call a JavaFX function from within a Java method. I needed to do this yesterday, so I thought I'd take a moment to explain a way to do this in three simple steps:
Create a Java interface
Extend the interface with a JavaFX class, implementing any functions defined by the interface
Obtain a reference to, and call a function of, the JavaFX class defined by the interface
Here are snippets of the code that I created yesterday in the context of the steps listed above:
Create the Java interface
public interface UniverseHandler { public void onSphereClicked(SphereBranch sphereBranch); }
Extend the interface with a JavaFX class, implementing any functions defined by the interface
public class WindshieldModel extends UniverseHandler { ...model code omitted...
/** * This is called when user clicks a sphere on the 3D Canvas */ public override function onSphereClicked(sphereBranch:SphereBranch) { curSphereBranch = sphereBranch; } }
Obtain a reference to, and call a function of, the JavaFX class defined by the interface
At the end of the day Friday, a couple of colleagues invited me to brainstorm with them about a 3D data visualization tool. I have heard that JavaFX will have 3D capabilities, perhaps yet this year, but I needed something sooner than that. After the session I searched for the latest developments in the intersection of JavaFX and Java 3D. I was pleasantly surprised by the great work that a German organization named InteractiveMesh.org has done in the 3D space relative to JavaFX.
As you may know, you can wrap a Java 3D canvas in a JavaFX component, but there are a couple of issues with this:
The 3D canvas is a heavyweight component, so a JavaFX node placed on top gets obscured.
Interfaces between the JavaFX scenegraph and the Java 3D universe would be required in order to provide a great user experience.
The screenshot above shows a Java 3D universe placed in a JavaFX program, and thanks to the libraries created by InteractiveMesh.org the 3D universe is rendered into a lightweight component. Also, their libraries provide for integration between the JavaFX scenegraph and the 3D universe (such as redirecting mouse events to the underlying heavyweight Canvas3D object).
Their site has a couple of JavaFX/Java 3D demos, the screenshot above showing one of them. When running the demos, be sure to discover the various interactions that the mouse provides. For example, you can scale the cube above with a mouse wheel, and can drag the cube while holding mouse button #2 down. Congratulations and thanks to InteractiveMesh.org, and to August Lammersdorf whose name is in the source code.
Recent Comments