With the new, improved, custom layout capabilities in JavaFX 1.3, I thought it'd be useful to post a simple example. This example defines a custom layout in JavaFX whose behavior is like the BorderLayout, familiar to most Java programmers. Here's a screen shot:
There are actually two ways to create custom layouts in JavaFX:
Sub-class javafx.scene.layout.Container
Create a javafx.scene.layout.Panel instance
Here is the code from today's example that employs the first way. Note that I'm leveraging the Node#id instance variable to supply the directional values needed by the traditional BorderLayout.
/*
* BorderLayout.fx -
* Example of subclassing Container to create a custom layout
*/
package containersubclasslayoutexample.layout;
import javafx.scene.layout.*;
import javafx.scene.layout.Container.*;
public class BorderLayout extends Container {
var topHeight:Number;
var bottomHeight:Number;
var leftWidth:Number;
var rightWidth:Number;
override function doLayout():Void {
for (node in getManaged(content)) {
if (node.id == "top") {
topHeight = getNodePrefHeight(node);
setNodeWidth(node, width);
setNodeHeight(node, getNodePrefHeight(node));
positionNode(node, 0, 0);
}
else if (node.id == "bottom") {
bottomHeight = getNodePrefHeight(node);
setNodeWidth(node, width);
setNodeHeight(node, getNodePrefHeight(node));
positionNode(node, 0, height - bottomHeight);
}
else if (node.id == "left") {
leftWidth = getNodePrefWidth(node);
setNodeWidth(node, getNodePrefWidth(node));
setNodeHeight(node, height - (topHeight + bottomHeight));
positionNode(node, 0, topHeight);
}
else if (node.id == "right") {
rightWidth = getNodePrefWidth(node);
setNodeWidth(node, getNodePrefWidth(node));
setNodeHeight(node, height - (topHeight + bottomHeight));
positionNode(node, width - rightWidth, topHeight);
}
else if (node.id == "center") {
setNodeWidth(node, width - (leftWidth + rightWidth));
setNodeHeight(node, height - (topHeight + bottomHeight));
positionNode(node, leftWidth, topHeight);
}
}
}
override function getPrefWidth(height:Number):Number {
return width;
}
override function getPrefHeight(width:Number):Number {
return height;
}
}
Here is a code example of using the BorderLayout shown above.
Lastly, here is an example of defining the BorderLayout custom layout functionality using the second way listed above, which is to create a Panel instance and override its behavior:
To learn more about using and creating layouts in JavaFX 1.3, see the blog posts that Amy Fowler has been writing on the subject, and the Custom Layouts chapter in the upcoming new edition of the Clarke/Bruno/Connors JavaFX book.
A note regarding the JavaFX RIA Exemplar Challenge: We have received several entries and I'm in the process of packaging them up and sharing them with the judges. The winner will be announced in early June, 2010.
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.
Recent Comments