Chris Oliver's Weblog
- All
- F3
- JavaFX
- Programming
- Research
Collada MoonBuggy in JavaFX
Collada MoonBuggy with skin animation in JavaFX.
In the scripting API for this scene graph, animated Collada models are Nodes which are also polymorphic with Timelines. You can "play" them. Relevant code:
var model = Model {
url: "lunar_lander_tris.dae"
repeatCount: Timeline.INDEFINITE
}
model.play();
var zoom = 0.0;
var scene = Scene {
background: BLACK;
camera: PerspectiveCamera {
heightAngle: 15
nearDistance: 1
farDistance: 1000
position: bind Transform.translate(0, 0, 30+zoom);
}
lights: [...]
content: model
}
...
Posted at 09:29AM Jan 13, 2009 by Christopher Oliver in Research | Comments[13]
Simple yet elegant vector user interfaces in JavaFX 1.0
It's very easy to create simple yet elegant custom vector user interface elements in JavaFX 1.0 by means of simple compositions of basic shapes. The above example consists entirely of compositions of simple triangles and (rounded) rectangles, together with some text.
The outer shell is a round rectangle from which two other round rectangles have been "subtracted", one for the control area, and one for the track of the slider. Behind this shape is a semi-transparent round rectangle of the same size. Due to the background color of the scene in the screenshot, you can't really tell, but the result is that you can partially "see through" these areas.
The "play", "back", and "forward", buttons are composed of a single triangle or two "added" together. The "pause" button consists of two rectangles "added" together. Finally, the thumb on the slider is simply a rectangle that's been rotated.
In JavaFX 1.0, you can declaratively compose vector shapes by means of the ShapeSubtract node. Although it's my personal opinion that this API element is poorly named and its member variables (a and b) overly obscure, nevertheless it's good enough to get the job done for now.
The a instance variable of ShapeSubtract takes a list of shapes which will be added together. Its b instance variable takes a list of shapes which will then be subtracted from that. ShapeSubtract is itself a shape and may be used in a larger composition.
Using JavaFX script, it's then very easy to factor such into reusable custom scene graph elements, and to make them interactive and/or animated.
Below is the full source code for the example.
/*
* Main.fx
*
*/
package moviecontrol;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.text.*;
import javafx.scene.*;
import javafx.scene.shape.*;
import javafx.scene.transform.*;
import javafx.scene.paint.*;
import javafx.scene.paint.Color.*;
import javafx.scene.input.*;
import java.lang.Math;
import java.lang.System;
import javafx.animation.*;
def defaultFillColor = Color.color(.8, .8, .8, 1);
def selectedFillColor = WHITE;
class MovieButton extends CustomNode {
// interface
public var action: function():Void;
public var icon: Shape;
public var selectedIcon: Shape;
public var selected: Boolean;
// implementation
var mouseOver: Boolean = bind hover;
var mousePress: Boolean = false;
var fillColor = bind if (mouseOver and mousePress) selectedFillColor else defaultFillColor;
var path = bind
if (selected and selectedIcon != null)
ShapeSubtract { fill: bind fillColor, a: selectedIcon }
else
ShapeSubtract { fill: bind fillColor, a: icon };
override protected function create():Node {
Group {
// center it
translateX: bind -path.boundsInLocal.width / 2;
translateY: bind -path.boundsInLocal.height / 2;
// mouse behavior
onMouseReleased: function(e) {
if (mouseOver) {
if (action != null) action();
selected = not selected;
}
mousePress = false;
}
onMousePressed: function(e) {
mousePress = true;
}
// make an internal scene consisting of the icon shape
// and an invisiable rectangle bounding it (so mouse
// events anywhere within its bounding box are
// accepted
content:
[Rectangle {
height: bind path.boundsInLocal.height;
width: bind path.boundsInLocal.width;
opacity: 0;
fill: Color.BLACK;
},
Group {
content: bind path;
}];
}
}
}
class MovieControl extends CustomNode {
public var back: function():Void;
public var fwd: function():Void;
public var paused: Boolean;
public var loaded: Duration;
public var setPosition: function(pos:Duration):Void;
public var duration: Duration = 0s on replace {
updateAlpha();
}
public var position: Duration = 0s on replace {
updateAlpha();
}
function updateAlpha():Void {
if (duration != null and position != null and duration != 0s) {
positionAlpha = position.toMillis() / duration.toMillis();
}
}
var positionAlpha: Number;
override protected function create():Node {
Group {
translateX: -150;
translateY: -32;
var bg:Rectangle;
content:
[bg = Rectangle { // semi-transparent background
height: 64;
width: 300;
arcHeight: 20;
arcWidth: 20;
fill: Color.color(0, 0, 0, 0.2);
},
ShapeSubtract { // subtract the control area and slider track from the main body
fill: defaultFillColor;
a: Rectangle {
height: 64;
width: 300;
arcHeight: 20;
arcWidth: 20;
}
b:
[Rectangle {
x: 1;
y: 1;
arcHeight: 20;
arcWidth: 20;
width: 298;
height: 48;
},
Rectangle {
x: 50;
y: 50;
height: 13;
width: 200;
arcHeight: 13;
arcWidth: 13;
}]
},
Group { // place the text for the elapsed time and duration
translateY: 52;
var font = Font {size: 11};
content:
[Text {
x: 10;
y: 0;
textOrigin: TextOrigin.TOP
font: font;
fill: BLACK;
content: bind "{%tM position}:{%tS position}";
},
Text {
x: 254;
y: 0;
textOrigin: TextOrigin.TOP
font: font;
fill: BLACK;
content: bind if (duration == null or position == null) "" else "-{%tM duration.sub(position)}:{%tS duration.sub(position)}";
}]
},
Group { // handle the slider thumb
var thumbX: Number = bind positionAlpha * 190;
translateX: bind 51 + thumbX;
translateY: 52.5;
var thumb: Rectangle;
var startX = 0.0;
onMousePressed: function(e) {
startX = thumbX;
}
onMouseDragged: function(e) {
var x = startX + e.dragX;
x = Math.max(Math.min(x, 190), 0);
positionAlpha = x / 190;
if (setPosition != null) { setPosition(position); };
}
content: thumb = Rectangle {
var c = 8.0;
transforms: Transform.rotate(45, c/2, c/2);
height: c;
width: c;
var thumbMousePress = false;
onMousePressed: function(e) {
thumbMousePress = true;
}
onMouseReleased: function(e) {
thumbMousePress = false;
}
fill: bind if (thumbMousePress) selectedFillColor else defaultFillColor;
}
},
Group { // construct the various buttons
translateX: 100;
translateY: 24;
// functions for basic shape elements that
// are composed below
var u = 16.0;
var bar = function() {
Rectangle {
height: u;
width: u/3
}
};
var leftArrow = function() {
Polygon {
points: [0, u/2, u, 0, u, u];
}
};
var rightArrow = function() {
Polygon {
points: [0, 0, u, u/2, 0, u];
}
}
var backIcon = function() {
ShapeSubtract {
a:
[leftArrow(),
ShapeSubtract {
translateX: u;
a: leftArrow()
}]
}
};
var fwdIcon = function() {
ShapeSubtract {
a: [rightArrow(),
ShapeSubtract {
translateX: u;
a: rightArrow()
}];
}
};
var playIcon = function() {
ShapeSubtract {
transforms:
[Transform.scale(1.5, 1.5)];
a: rightArrow();
}
};
var pauseIcon = function() {
ShapeSubtract {
transforms:
[Transform.scale(1.5, 1.5)];
a:
[bar(), ShapeSubtract { translateX: u/2; a: bar()}];
}
};
content:
Group {
var buttons =
[MovieButton {
icon: backIcon();
action: bind back;
},
MovieButton {
icon: pauseIcon();
selected: bind paused with inverse;
selectedIcon: playIcon()
},
MovieButton {
icon: fwdIcon();
action: bind fwd;
}];
content: for (i in buttons)
Group {
translateX: indexof i * 42;
content: i;
}
}
}]
}
}
}
/**
* @author coliver
*/
// As a test simulate playing movies with a timeline
var duration = 5m;
function reset():Void {
simulator.stop();
paused = true;
}
var simulator = Timeline {
keyFrames:
KeyFrame {
time: duration
}
repeatCount: Timeline.INDEFINITE;
};
var paused = true on replace {
if (paused) { simulator.pause() } else { simulator.play() }
}
Stage{
title: "Movie Control"
width: 500
height: 400
scene: Scene{
fill: BLACK;
content: MovieControl {
translateX: 250
translateY: 180
setPosition: function(pos:Duration) {
simulator.time = pos;
}
fwd: reset
back: reset
paused: bind paused with inverse;
duration: bind duration;
position: bind simulator.time with inverse;
}
}
}
Posted at 10:42AM Jan 08, 2009 by Christopher Oliver in JavaFX | Comments[7]
JavaFX Script Keywords and Java Interoperability
In JavaFX script any sequence of characters enclosed in "french quotes" is treated as a lexical identifier, and thus may be used as a valid name of a variable, function, or class.
var <<this is a variable>> = "Hello World"; <<this is a variable>>.toUpperCase();
This mechanism may be used to access Java methods which conflict with JavaFX keywords, for example:
var textArea = new javax.swing.JTextArea();
textArea.<<insert>>("Hello World", 0);
This mechanism is also useful for code generators in translating symbols from other languages having incompatible lexical rules to JavaFX script.
Posted at 06:52PM Jan 07, 2009 by Christopher Oliver in JavaFX | Comments[8]
From F3 to JavaFX 1.0 - Effects
An important and impressive innovation between F3 and JavaFX is the Effects framework created by Chris Campbell.
F3 had a simple system of software pixel filters, which could be applied to any Node or group of Nodes in a scene. However, thanks to Chris, JavaFX 1.0 includes a much more complete set of effects, and a sophisticated framework that enables GPU hardware acceleration where available.
Underlying the simple declarative expression of effects at the JavaFX script level, effect implementations are described in a GPU-shader-like procedural language, which Chris created, called JSL. Chris's JSL compiler then compiles to various targets, either GPU-based (GLSL/HLSL), or CPU-based (Java/Native).
Posted at 07:45AM Jan 05, 2009 by Christopher Oliver in JavaFX | Comments[8]
Performance matters - 25x for JavaFX script over Groovy and JRuby
JavaFX script
function tak(x:Number, y:Number, z:Number): Number {
if (y >= x) z else tak(tak(x-1, y, z),
tak(y-1, z, x),
tak(z-1, x, y));
}
for (i in [1..1000]) {
tak(24, 16, 8);
}
time javafx -server -cp . Tak real 0m10.724s user 0m10.105s sys 0m0.173sGroovy
def tak(double x, double y, double z) {
return y >= x ? z : tak(tak(x-1, y, z),
tak(y-1, z, x),
tak(z-1, x, y));
}
int i = 0;
while (i++ < 1000) {
tak(24, 16, 8);
}
time java -Djava.ext.dirs=./groovy-1.6-RC-1/lib -server groovy.lang.GroovyShell tak.groovy real 4m36.674s user 4m29.272s sys 0m3.842s
JRuby
def tak x, y, z
unless y < x
z
else
tak( tak(x-1, y, z),
tak(y-1, z, x),
tak(z-1, x, y))
end
end
i = 0
while i<1000
tak(24, 16, 8)
i+=1
end
time ./jruby-1.1.6RC1/bin/jruby -J-server tak.rb real 4m24.735s user 4m22.203s sys 0m1.069s
Summary
For this benchmark, as you can see both JRuby and Groovy are around 25x slower than JavaFX script.
Posted at 05:21PM Jan 02, 2009 by Christopher Oliver in JavaFX | Comments[46]