Recommended: Sing it, brah! 5 fabulous songs for developers
JW's Top 5
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 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 {
height: 64;
width: 300;
arcHeight: 20;
arcWidth: 20;
fill: Color.color(0, 0, 0, 0.2);
},
ShapeSubtract {
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 {
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 {
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 {
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;
}
}
}