How to set axis (triad) at fixed position on screen in JavaFX? - java

How to set axis (triad) at fixed position on screen in JavaFX? I am currently developing one application in which I want to show axis (triad) at fixed position on my screen (i.e. bottom-left corner). I want rotation of axis should be in sync with the main object. Zoom and Translate operation should not be applied to axis.
But I am facing some difficulties to show axis at specific position on screen.
I have used screenToLocal method to get fixed position in scene but it only returns Point2D object which is not helpful to set 3D translate values.
Can you please give me solution for this problem?
Source code based on this example is as below:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Point3D;
import javafx.scene.DepthTest;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.Material;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.Cylinder;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.Sphere;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
public class TrafoTest extends Application {
final Group root = new Group();
Group axis = new Group();
final XformWorld world = new XformWorld();
final PerspectiveCamera camera = new PerspectiveCamera(true);
final XformCamera cameraXform = new XformCamera();
final XformCamera cameraXform2 = new XformCamera();
final XformCamera cameraXform3 = new XformCamera();
private static final double CAMERA_INITIAL_DISTANCE = -1000;
private static final double CAMERA_NEAR_CLIP = 0.1;
private static final double CAMERA_FAR_CLIP = 10000.0;
private static final double MOUSE_SPEED = 1;
private static final double ROTATION_SPEED = 4.0;
private static final double TRACK_SPEED = 0.02;
double mousePosX, mousePosY, mouseOldX, mouseOldY, mouseDeltaX, mouseDeltaY;
double mouseFactorX, mouseFactorY;
Stage stage;
#Override
public void start(Stage primaryStage) {
root.getChildren().add(world);
root.setDepthTest(DepthTest.ENABLE);
buildCamera();
buildBodySystem();
Scene scene = new Scene(root, 800, 600, true);
scene.setFill(Color.GREY);
handleMouse(scene);
this.stage = primaryStage;
primaryStage.setTitle("TrafoTest");
primaryStage.setScene(scene);
primaryStage.show();
scene.setCamera(camera);
mouseFactorX = 180.0 / scene.getWidth();
mouseFactorY = 180.0 / scene.getHeight();
}
private void buildCamera() {
root.getChildren().add(cameraXform);
cameraXform.getChildren().add(cameraXform2);
cameraXform2.getChildren().add(cameraXform3);
cameraXform3.getChildren().add(camera);
camera.setNearClip(CAMERA_NEAR_CLIP);
camera.setFarClip(CAMERA_FAR_CLIP);
camera.setTranslateZ(CAMERA_INITIAL_DISTANCE);
}
private void buildBodySystem() {
PhongMaterial whiteMaterial = new PhongMaterial();
whiteMaterial.setDiffuseColor(Color.WHITE);
whiteMaterial.setSpecularColor(Color.LIGHTBLUE);
Box box = new Box(400, 200, 100);
box.setMaterial(whiteMaterial);
box.setDrawMode(DrawMode.LINE);
PhongMaterial redMaterial = new PhongMaterial();
redMaterial.setDiffuseColor(Color.DARKRED);
redMaterial.setSpecularColor(Color.RED);
Sphere sphere = new Sphere(5);
sphere.setMaterial(redMaterial);
sphere.setTranslateX(200.0);
sphere.setTranslateY(-100.0);
sphere.setTranslateZ(-50.0);
axis = drawReferenceFrame();
world.getChildren().addAll(axis);
world.getChildren().add(box);
world.getChildren().addAll(sphere);
}
private void handleMouse(Scene scene) {
scene.setOnMousePressed((MouseEvent me) -> {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
});
scene.setOnMouseDragged((MouseEvent me) -> {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
if (me.isPrimaryButtonDown()) {
cameraXform.ry(mouseDeltaX * 180.0 / scene.getWidth());
cameraXform.rx(-mouseDeltaY * 180.0 / scene.getHeight());
BoundingBox point = (BoundingBox) root.screenToLocal(new BoundingBox(root.getLayoutX()+350, root.getLayoutY()+650, 0, 0,0, 20));
System.out.println(point);
axis.setTranslateX(point.getMinX());
axis.setTranslateY(point.getMinY());
axis.setTranslateZ(point.getMinZ());
} else if (me.isSecondaryButtonDown()) {
cameraXform2.setTx((cameraXform2.t.getX() + (-mouseDeltaX)*MOUSE_SPEED*TRACK_SPEED));
cameraXform2.setTy((cameraXform2.t.getY() + (-mouseDeltaY)*MOUSE_SPEED*TRACK_SPEED));
camera.setTranslateZ(camera.getTranslateZ() + mouseDeltaY);
}
});
scene.setOnScroll(new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double z = cameraXform3.getTranslateZ();
double newZ = z - event.getDeltaY() * MOUSE_SPEED * 0.05;
cameraXform3.setTranslateZ(newZ);
}
});
}
public static void main(String[] args) {
launch(args);
}
private Group drawReferenceFrame(){
Group G1= new Group();
Cylinder CX = new Cylinder(2,25);
Cylinder CY = new Cylinder(2,25);
Cylinder CZ = new Cylinder(2,25);
Sphere S = new Sphere(4);
Material mat =new PhongMaterial(Color.WHITE);
PhongMaterial Xmat = new PhongMaterial();
Xmat.setDiffuseColor(Color.GREEN);
PhongMaterial Ymat = new PhongMaterial();
Ymat.setDiffuseColor(Color.BLUE);
PhongMaterial Zmat = new PhongMaterial();
Zmat.setDiffuseColor(Color.RED);
S.setMaterial(Zmat);
CY.setMaterial(mat);
// CY.setRotationAxis(Rotate.X_AXIS);
// CY.setRotate(90);
CY.setTranslateY(-12.5);
CX.setMaterial(mat);
CX.setTranslateX(15);
CX.setRotationAxis(Rotate.Z_AXIS);
CX.setRotate(90);
CZ.setMaterial(mat);
CZ.setRotationAxis(Rotate.X_AXIS);
CZ.setRotate(90);
CZ.setTranslateZ(-12.5);
G1.getChildren().addAll(CX,CY,CZ,S);
TriangleMesh coneMeshY = createCone(3.5f, 7.5f);
TriangleMesh coneMeshX = createCone(3.5f, 7.5f);
TriangleMesh coneMeshZ = createCone(3.5f, 7.5f);
MeshView yCone = new MeshView(coneMeshY);
MeshView xCone = new MeshView(coneMeshX);
MeshView zCone = new MeshView(coneMeshZ);
yCone.setMaterial(Ymat);
yCone.setTranslateY(-32.5);
yCone.setDrawMode(DrawMode.FILL);
xCone.setMaterial(Xmat);
xCone.setTranslateY(-3.75);
xCone.setRotationAxis(Rotate.Z_AXIS);
xCone.setRotate(90);
xCone.setTranslateX(28.5);
xCone.setDrawMode(DrawMode.FILL);
zCone.setRotationAxis(Rotate.X_AXIS);
zCone.setTranslateY(-3.75);
zCone.setRotate(90);
zCone.setTranslateZ(-28.5);
zCone.setDrawMode(DrawMode.FILL);
zCone.setMaterial(Zmat);
G1.getChildren().addAll(xCone,yCone,zCone);
// G1.setScale(0.45);
return G1;
}
private TriangleMesh createCone( float radius, float height) {
int divisions=500;
TriangleMesh mesh = new TriangleMesh();
mesh.getPoints().addAll(0,0,0);
double segment_angle = 2.0 * Math.PI / divisions;
float x, z;
double angle;
double halfCount = (Math.PI / 2 - Math.PI / (divisions / 2));
for(int i=divisions+1;--i >= 0; ) {
angle = segment_angle * i;
x = (float)(radius * Math.cos(angle - halfCount));
z = (float)(radius * Math.sin(angle - halfCount));
mesh.getPoints().addAll(x,height,z);
}
mesh.getPoints().addAll(0,height,0);
mesh.getTexCoords().addAll(0,0);
for(int i=1;i<=divisions;i++) {
mesh.getFaces().addAll(
0,0,i+1,0,i,0, //COunter clock wise
divisions+2,0,i,0,i+1,0 // Clock wise
);
}
return mesh;
}
}
class XformWorld extends Group {
final Translate t = new Translate(0.0, 0.0, 0.0);
final Rotate rx = new Rotate(0, 0, 0, 0, Rotate.X_AXIS);
final Rotate ry = new Rotate(0, 0, 0, 0, Rotate.Y_AXIS);
final Rotate rz = new Rotate(0, 0, 0, 0, Rotate.Z_AXIS);
public XformWorld() {
super();
this.getTransforms().addAll(t, rx, ry, rz);
}
}
class XformCamera extends Group {
Point3D px = new Point3D(1.0, 0.0, 0.0);
Point3D py = new Point3D(0.0, 1.0, 0.0);
Rotate r;
Transform tx = new Rotate();
Translate t = new Translate();
public XformCamera() {
super();
}
public void rx(double angle) {
r = new Rotate(angle, px);
this.tx = tx.createConcatenation(r);
this.getTransforms().clear();
this.getTransforms().addAll(tx);
}
public void ry(double angle) {
r = new Rotate(angle, py);
this.tx = tx.createConcatenation(r);
this.getTransforms().clear();
this.getTransforms().addAll(tx);
}
public void setTx(double x) {
t.setX(x);
this.getTransforms().clear();
this.getTransforms().addAll(t);
}
public void setTy(double y) {
t.setY(y);
this.getTransforms().clear();
this.getTransforms().addAll(t);
}
}
In above code, first I have translated the axis at left-bottom corner of my screen, but after rotating the main object (ie. BOX and SPHERE), axis is also translated.I wanted to rotate main object and axis about their own origins.
If I tried to zoom in (changed camera Z position), then also I axis location changed with respect to screen.
In above cases, I wanted to display my axis at left-bottom corner of the screen and Rotation of the axis should be in sync with the main object.

I wanted the axis to be fixed at left, bottom corner of the screen and axis should rotate about it's own origin.
This variation leaves the axes at the origin, moves the box to
P = (size / 2, -size / 2, -size / 2)
relative to the axes, and pans the camera toward the bottom, center of the screen. Uncomment the call to camera.setTranslateX() to pan left. Moving the mouse causes the group to rotate about the axes' origin. Press shift to rotate about z, and use the mouse wheel to dolly the camera.
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
/**
* #see http://stackoverflow.com/a/37734966/230513
* #see http://stackoverflow.com/a/37714700/230513
* #see http://stackoverflow.com/a/37685167/230513
* #see http://stackoverflow.com/a/37370840/230513
*/
public class TriadBox extends Application {
private static final double SIZE = 300;
private final Content content = Content.create(SIZE);
private static final class Content {
private static final double WIDTH = 3;
private final Group group = new Group();
private final Rotate rx = new Rotate(0, Rotate.X_AXIS);
private final Rotate ry = new Rotate(0, Rotate.Y_AXIS);
private final Rotate rz = new Rotate(0, Rotate.Z_AXIS);
private final Box xAxis;
private final Box yAxis;
private final Box zAxis;
private final Box box;
private static Content create(double size) {
Content c = new Content(size);
c.group.getChildren().addAll(c.box, c.xAxis, c.yAxis, c.zAxis);
c.group.getTransforms().addAll(c.rz, c.ry, c.rx);
return c;
}
private Content(double size) {
xAxis = createBox(size * 2, WIDTH, WIDTH);
yAxis = createBox(WIDTH, size * 2, WIDTH);
zAxis = createBox(WIDTH, WIDTH, size * 2);
double edge = 3 * size / 4;
box = new Box(edge, edge, edge);
box.setMaterial(new PhongMaterial(Color.CORAL));
box.setTranslateX(size / 2);
box.setTranslateY(-size / 2);
box.setTranslateZ(-size / 2);
}
private Box createBox(double w, double h, double d) {
Box b = new Box(w, h, d);
b.setMaterial(new PhongMaterial(Color.AQUA));
return b;
}
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("JavaFX 3D");
Scene scene = new Scene(content.group, SIZE * 2, SIZE * 2, true);
primaryStage.setScene(scene);
scene.setFill(Color.BLACK);
scene.setOnMouseMoved((final MouseEvent e) -> {
if (e.isShiftDown()) {
content.rz.setAngle(e.getSceneX() * 360 / scene.getWidth());
} else {
content.rx.setAngle(e.getSceneY() * 360 / scene.getHeight());
content.ry.setAngle(e.getSceneX() * 360 / scene.getWidth());
}
});
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setFarClip(SIZE * 6);
//camera.setTranslateX(SIZE / 2);
camera.setTranslateY(-SIZE / 2);
camera.setTranslateZ(-4.5 * SIZE);
scene.setCamera(camera);
scene.setOnScroll((final ScrollEvent e) -> {
camera.setTranslateZ(camera.getTranslateZ() + e.getDeltaY());
});
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

It may be easier to have separate groups for the rotatable content versus the fixed triad. In the example below, cube contains a single child, box, and three transforms, rx, ry and rz. In contrast, axes contains three axes with no transforms. The axes are translated to the lower left corner as a group, leaving the mouse handler free to manipulate the Rotate instances belonging to cube.
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
/**
* #see http://stackoverflow.com/a/37685167/230513
* #see http://stackoverflow.com/a/37370840/230513
*/
public class TriadBox extends Application {
private static final double SIZE = 300;
private final Content content = Content.create(SIZE);
private static final class Content {
private static final double WIDTH = 3;
private final Group group = new Group();
private final Group cube = new Group();
private final Group axes = new Group();
private final Rotate rx = new Rotate(0, Rotate.X_AXIS);
private final Rotate ry = new Rotate(0, Rotate.Y_AXIS);
private final Rotate rz = new Rotate(0, Rotate.Z_AXIS);
private final Box xAxis;
private final Box yAxis;
private final Box zAxis;
private final Box box;
private static Content create(double size) {
Content c = new Content(size);
c.cube.getChildren().add(c.box);
c.cube.getTransforms().addAll(c.rz, c.ry, c.rx);
c.axes.getChildren().addAll(c.xAxis, c.yAxis, c.zAxis);
c.axes.setTranslateX(-size / 2);
c.axes.setTranslateY(size / 2);
c.group.getChildren().addAll(c.cube, c.axes);
return c;
}
private Content(double size) {
xAxis = createBox(size*2, WIDTH, WIDTH);
yAxis = createBox(WIDTH, size*2, WIDTH);
zAxis = createBox(WIDTH, WIDTH, size*2);
double edge = 3 * SIZE / 4;
box = new Box(edge, edge, edge);
box.setMaterial(new PhongMaterial(Color.CORAL));
}
private Box createBox(double w, double h, double d) {
Box b = new Box(w, h, d);
b.setMaterial(new PhongMaterial(Color.AQUA));
return b;
}
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("JavaFX 3D");
Scene scene = new Scene(content.group, SIZE * 2, SIZE * 2, true);
primaryStage.setScene(scene);
scene.setFill(Color.BLACK);
scene.setOnMouseMoved((final MouseEvent e) -> {
content.rx.setAngle(e.getSceneY() * 360 / scene.getHeight());
content.ry.setAngle(e.getSceneX() * 360 / scene.getWidth());
});
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setFarClip(SIZE * 6);
camera.setTranslateZ(-3 * SIZE);
scene.setCamera(camera);
scene.setOnScroll((final ScrollEvent e) -> {
camera.setTranslateZ(camera.getTranslateZ() + e.getDeltaY());
});
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

How to set axis (triad) at fixed position…I also wanted axis rotation in sync with box.
In this example, the Content.create() factory method translates the triad of axes is to the point
P = (-size / 2, size / 2, 0)
Because its separate group has no transforms, the triad's origin "sticks" to that point in the space viewed by the camera.
In contrast, the example below translates the triad of axes is to the point
P = (-size / 2, size / 2, size / 2)
Because the axes are in the same group as box, they undergo the same rotations.
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
/**
* #see https://stackoverflow.com/a/37714700/230513
* #see https://stackoverflow.com/a/37685167/230513
* #see https://stackoverflow.com/a/37370840/230513
*/
public class TriadBox extends Application {
private static final double SIZE = 300;
private final Content content = Content.create(SIZE);
private static final class Content {
private static final double WIDTH = 3;
private final Group group = new Group();
private final Rotate rx = new Rotate(0, Rotate.X_AXIS);
private final Rotate ry = new Rotate(0, Rotate.Y_AXIS);
private final Rotate rz = new Rotate(0, Rotate.Z_AXIS);
private final Box xAxis;
private final Box yAxis;
private final Box zAxis;
private final Box box;
private static Content create(double size) {
Content c = new Content(size);
c.group.getChildren().addAll(c.box, c.xAxis, c.yAxis, c.zAxis);
c.group.getTransforms().addAll(c.rz, c.ry, c.rx);
return c;
}
private Content(double size) {
xAxis = createBox(size * 2, WIDTH, WIDTH);
xAxis.setTranslateY(size / 2);
xAxis.setTranslateZ(size / 2);
yAxis = createBox(WIDTH, size * 2, WIDTH);
yAxis.setTranslateX(-size / 2);
yAxis.setTranslateZ(size / 2);
zAxis = createBox(WIDTH, WIDTH, size * 2);
zAxis.setTranslateX(-size / 2);
zAxis.setTranslateY(size / 2);
double edge = 3 * size / 4;
box = new Box(edge, edge, edge);
box.setMaterial(new PhongMaterial(Color.CORAL));
}
private Box createBox(double w, double h, double d) {
Box b = new Box(w, h, d);
b.setMaterial(new PhongMaterial(Color.AQUA));
return b;
}
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("JavaFX 3D");
Scene scene = new Scene(content.group, SIZE * 2, SIZE * 2, true);
primaryStage.setScene(scene);
scene.setFill(Color.BLACK);
scene.setOnMouseMoved((final MouseEvent e) -> {
if (e.isShiftDown()) {
content.rz.setAngle(e.getSceneX() * 360 / scene.getWidth());
} else {
content.rx.setAngle(e.getSceneY() * 360 / scene.getHeight());
content.ry.setAngle(e.getSceneX() * 360 / scene.getWidth());
}
});
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setFarClip(SIZE * 6);
camera.setTranslateZ(-3.5 * SIZE);
scene.setCamera(camera);
scene.setOnScroll((final ScrollEvent e) -> {
camera.setTranslateZ(camera.getTranslateZ() + e.getDeltaY());
});
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

I wanted to rotate both main object and axis about their own center.
For that, I would return to the original example that has separate groups for the content and triad. The variation below adds the same threes Rotate transforms to both cube and axes. As a result, the setOnMouseMoved() implementation causes the the two to rotate in synchrony. Mouse over the stage to see the effect.
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
/**
* #see https://stackoverflow.com/a/37743539/230513
* #see https://stackoverflow.com/a/37370840/230513
*/
public class TriadBox extends Application {
private static final double SIZE = 300;
private final Content content = Content.create(SIZE);
private static final class Content {
private static final double WIDTH = 3;
private final Group group = new Group();
private final Group cube = new Group();
private final Group axes = new Group();
private final Rotate rx = new Rotate(0, Rotate.X_AXIS);
private final Rotate ry = new Rotate(0, Rotate.Y_AXIS);
private final Rotate rz = new Rotate(0, Rotate.Z_AXIS);
private final Box xAxis;
private final Box yAxis;
private final Box zAxis;
private final Box box;
private static Content create(double size) {
Content c = new Content(size);
c.cube.getChildren().add(c.box);
c.cube.getTransforms().addAll(c.rz, c.ry, c.rx);
c.cube.setTranslateX(size / 2);
c.axes.getChildren().addAll(c.xAxis, c.yAxis, c.zAxis);
c.axes.getTransforms().addAll(c.rz, c.ry, c.rx);
c.axes.setTranslateX(-size / 2);
c.group.getChildren().addAll(c.cube, c.axes);
return c;
}
private Content(double size) {
xAxis = createBox(size, WIDTH, WIDTH);
yAxis = createBox(WIDTH, size, WIDTH);
zAxis = createBox(WIDTH, WIDTH, size);
double edge = 3 * size / 5;
box = new Box(edge, edge, edge);
box.setMaterial(new PhongMaterial(Color.CORAL));
}
private Box createBox(double w, double h, double d) {
Box b = new Box(w, h, d);
b.setMaterial(new PhongMaterial(Color.AQUA));
return b;
}
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("JavaFX 3D");
Scene scene = new Scene(content.group, SIZE * 2, SIZE * 2, true);
primaryStage.setScene(scene);
scene.setFill(Color.BLACK);
scene.setOnMouseMoved((final MouseEvent e) -> {
if (e.isShiftDown()) {
content.rz.setAngle(e.getSceneX() * 360 / scene.getWidth());
} else {
content.rx.setAngle(e.getSceneY() * 360 / scene.getHeight());
content.ry.setAngle(e.getSceneX() * 360 / scene.getWidth());
}
});
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setFarClip(SIZE * 6);
camera.setTranslateZ(-4 * SIZE);
scene.setCamera(camera);
scene.setOnScroll((final ScrollEvent e) -> {
camera.setTranslateZ(camera.getTranslateZ() + e.getDeltaY());
});
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

In order to see what you were seeing, I made these changes to TrafoTest and set the scene to black.
I then modified this example to make the scene look more like yours, adding a sphere on one corner and shifting the axes to the corner opposite. I substituted a variation of your custom rotation Group and your mouse handler from here. The new group also rotates around z when isShiftDown(). Also note the slightly simpler camera dolly in the setOnScroll() handler. In particular, note that the axes can be translated independently as a group. Try this variation in Content.create().
c.axes.setTranslateX(c.axes.getTranslateX() - 20);
c.axes.setTranslateY(c.axes.getTranslateY() + 20);
c.axes.setTranslateZ(c.axes.getTranslateZ() + 20);
import javafx.application.Application;
import javafx.geometry.Point3D;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Transform;
import javafx.stage.Stage;
/**
* #see https://stackoverflow.com/a/37755149/230513
* #see https://stackoverflow.com/a/37743539/230513
* #see https://stackoverflow.com/a/37370840/230513
*/
public class TriadBox extends Application {
private static final double SIZE = 300;
private final Content content = Content.create(SIZE);
private double mousePosX, mousePosY, mouseOldX, mouseOldY, mouseDeltaX, mouseDeltaY;
private static final class Content {
private static final double WIDTH = 3;
private final Xform group = new Xform();
private final Group cube = new Group();
private final Group axes = new Group();
private final Box xAxis;
private final Box yAxis;
private final Box zAxis;
private final Box box;
private final Sphere sphere;
private static Content create(double size) {
Content c = new Content(size);
c.cube.getChildren().addAll(c.box, c.sphere);
c.axes.getChildren().addAll(c.xAxis, c.yAxis, c.zAxis);
c.group.getChildren().addAll(c.cube, c.axes);
return c;
}
private Content(double size) {
double edge = 3 * size / 4;
xAxis = createBox(edge, WIDTH, WIDTH, edge);
yAxis = createBox(WIDTH, edge / 2, WIDTH, edge);
zAxis = createBox(WIDTH, WIDTH, edge / 4, edge);
box = new Box(edge, edge / 2, edge / 4);
box.setDrawMode(DrawMode.LINE);
sphere = new Sphere(8);
PhongMaterial redMaterial = new PhongMaterial();
redMaterial.setDiffuseColor(Color.CORAL.darker());
redMaterial.setSpecularColor(Color.CORAL);
sphere.setMaterial(redMaterial);
sphere.setTranslateX(edge / 2);
sphere.setTranslateY(-edge / 4);
sphere.setTranslateZ(-edge / 8);
}
private Box createBox(double w, double h, double d, double edge) {
Box b = new Box(w, h, d);
b.setMaterial(new PhongMaterial(Color.AQUA));
b.setTranslateX(-edge / 2 + w / 2);
b.setTranslateY(edge / 4 - h / 2);
b.setTranslateZ(edge / 8 - d / 2);
return b;
}
}
private static class Xform extends Group {
private final Point3D px = new Point3D(1.0, 0.0, 0.0);
private final Point3D py = new Point3D(0.0, 1.0, 0.0);
private Rotate r;
private Transform t = new Rotate();
public void rx(double angle) {
r = new Rotate(angle, px);
this.t = t.createConcatenation(r);
this.getTransforms().clear();
this.getTransforms().addAll(t);
}
public void ry(double angle) {
r = new Rotate(angle, py);
this.t = t.createConcatenation(r);
this.getTransforms().clear();
this.getTransforms().addAll(t);
}
public void rz(double angle) {
r = new Rotate(angle);
this.t = t.createConcatenation(r);
this.getTransforms().clear();
this.getTransforms().addAll(t);
}
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("JavaFX 3D");
Scene scene = new Scene(content.group, SIZE * 2, SIZE * 2, true);
primaryStage.setScene(scene);
scene.setFill(Color.BLACK);
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setFarClip(SIZE * 6);
camera.setTranslateZ(-2 * SIZE);
scene.setCamera(camera);
scene.setOnMousePressed((MouseEvent e) -> {
mousePosX = e.getSceneX();
mousePosY = e.getSceneY();
mouseOldX = e.getSceneX();
mouseOldY = e.getSceneY();
});
scene.setOnMouseDragged((MouseEvent e) -> {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = e.getSceneX();
mousePosY = e.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
if (e.isShiftDown()) {
content.group.rz(-mouseDeltaX * 180.0 / scene.getWidth());
} else if (e.isPrimaryButtonDown()) {
content.group.rx(+mouseDeltaY * 180.0 / scene.getHeight());
content.group.ry(-mouseDeltaX * 180.0 / scene.getWidth());
} else if (e.isSecondaryButtonDown()) {
camera.setTranslateX(camera.getTranslateX() - mouseDeltaX * 0.1);
camera.setTranslateY(camera.getTranslateY() - mouseDeltaY * 0.1);
camera.setTranslateZ(camera.getTranslateZ() + mouseDeltaY);
}
});
scene.setOnScroll((final ScrollEvent e) -> {
camera.setTranslateZ(camera.getTranslateZ() + e.getDeltaY());
});
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Related

Creating a 3d dice cube in javafx and doing its 3D dice animation

I've implemented a dice cube for a game I am developing with some partners and I am trying to create an animation for the cube.
Up until now the only thing I am able to achieve is to Rotate the cube 360 degrees for a duration of 3000 mils I set in an axis pattern I randomly create.
I however am not able to make a rotation on a Y axis and I am always landing on the same face of the cube(the number 4 for some reason).
here is the code that deals with the above text:
package test;
import javafx.application.Application;
import java.util.Random;
import javafx.animation.RotateTransition;
import javafx.scene.DepthTest;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TrafoTest extends Application {
final Group root = new Group();
final XformWorld world1 = new XformWorld();
final PerspectiveCamera camera = new PerspectiveCamera(true);
final XformCamera cameraXform = new XformCamera();
private static final double CAMERA_INITIAL_DISTANCE = -1000;
private static final double CAMERA_NEAR_CLIP = 0.1;
private static final double CAMERA_FAR_CLIP = 10000.0;
double mousePosX, mousePosY, mouseOldX, mouseOldY, mouseDeltaX, mouseDeltaY;
#Override
public void start(Stage primaryStage) {
root.getChildren().add(world1);
CuboidMesh cuboid = new CuboidMesh(50f,50f,50f);
cuboid.setTextureModeImage(getClass().getResource("/test/allFacesv2.png").toExternalForm());
RotateTransition rt = new RotateTransition(Duration.millis(3000), cuboid);
float number1;
float number2;
float number3;
float minX = 1.0f;
float maxX = 50.0f;
Random rand = new Random();
number1 = rand.nextFloat()* (maxX - minX) * minX;
number2 = rand.nextFloat()* (maxX - minX) + minX;
number3 = rand.nextFloat()* (maxX - minX) - minX;
rt.setByAngle(360);
rt.setAxis(new javafx.geometry.Point3D(number1,number2,number3));
// rt.setCycleCount(2);
//rt.setAutoReverse(true);
rt.play();
root.getChildren().addAll(cuboid);
root.setDepthTest(DepthTest.ENABLE);
buildCamera();
Scene scene = new Scene(root, 800, 800, true);
scene.setFill(Color.GREY);
handleMouse(scene);
primaryStage.setTitle("Transformationen");
primaryStage.setScene(scene);
primaryStage.show();
scene.setCamera(camera);
}
private void buildCamera() {
root.getChildren().add(cameraXform);
cameraXform.getChildren().add(camera);
camera.setNearClip(CAMERA_NEAR_CLIP);
camera.setFarClip(CAMERA_FAR_CLIP);
camera.setTranslateZ(CAMERA_INITIAL_DISTANCE);
}
private void handleMouse(Scene scene) {
scene.setOnMousePressed((MouseEvent me) -> {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
// this is done after clicking and the rotations are apearently
// performed in coordinates that are NOT rotated with the camera.
// (pls activate the two lines below for clicking)
//cameraXform.rx.setAngle(-90.0);
//cameraXform.ry.setAngle(180.0);
});
scene.setOnMouseDragged((MouseEvent me) -> {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
if (me.isPrimaryButtonDown()) {
// this is done when the mouse is dragged and each rotation is
// performed in coordinates, that are rotated with the camera.
cameraXform.ry.setAngle(cameraXform.ry.getAngle() + mouseDeltaX * 0.2);
cameraXform.rx.setAngle(cameraXform.rx.getAngle() - mouseDeltaY * 0.2);
}
});
}
public static void main(String[] args) {
launch(args);
}
}
class XformWorld extends Group {
final Translate t = new Translate(0.0, 0.0, 0.0);
final Rotate rx = new Rotate(0, 0, 0, 0, Rotate.X_AXIS);
final Rotate ry = new Rotate(0, 0, 0, 0, Rotate.Y_AXIS);
final Rotate rz = new Rotate(0, 0, 0, 0, Rotate.Z_AXIS);
public XformWorld() {
super();
this.getTransforms().addAll(t, rx, ry, rz);
}
}
class XformCamera extends Group {
final Translate t = new Translate(0.0, 0.0, 0.0);
final Rotate rx = new Rotate(0, 0, 0, 0, Rotate.X_AXIS);
final Rotate ry = new Rotate(0, 0, 0, 0, Rotate.Y_AXIS);
final Rotate rz = new Rotate(0, 0, 0, 0, Rotate.Z_AXIS);
public XformCamera() {
super();
this.getTransforms().addAll(t, rx, ry, rz);
}
}
the image I used is:

ScalaFX/JavaFX 8 get nearest Nodes

Currently experimenting with ScalaFX a bit.
Imagine the following:
I have some nodes and they are connected by some edges.
Now when I click the mousebutton I want to select the ones next to the mouse click, e.g. if I click between 1 and 2, I want those two to be selected, if I click before 0, only that one (as it's the first) etc.
Currently (and just as a proof of concept) I am doing this by adding in some helper structures. I have a HashMap of type [Index, Node] and select them like so:
wrapper.onMouseClicked = (mouseEvent: MouseEvent) =>
{
val lowerIndex: Int = (mouseEvent.sceneX).toString.charAt(0).asDigit
val left = nodes.get(lowerIndex)
val right = nodes.get(lowerIndex+1)
left.get.look.setStyle("-fx-background-color: orange;")
right.get.look.setStyle("-fx-background-color: orange;")
}
this does it's just, but I need to have an additional datastructure and it will get really tedious in 2D, like when I have a Y coordinate as well.
What I would prefer would be some method like mentioned in
How to detect Node at specific point in JavaFX?
or
JavaFX 2.2 get node at coordinates (visual tree hit testing)
These questions are based on older versions of JavaFX and use deprecated methods.
I could not find any replacement or solution in ScalaFX 8 so far. Is there a nice way to get all the nodes within a certain radius?
So "Nearest neighbor search" is the general problem you are trying to solve.
Your problem statement is a bit short on details. E.g., are nodes equidistant from each other? are nodes arranged in a grid pattern or randomly? is the node distance modeled based upon a point at the node center, a surrounding box, the actual closest point on an arbitrarily shaped node? etc.
I'll assume randomly placed shapes that may overlap, and picking is not based upon painting order, but on the closest corners of the bounding boxes of shapes. A more accurate picker might work by comparing the clicked point against against an elliptical area surrounding the actual shape rather than the shapes bounding box (as the current picker will be a bit finicky to use for things like overlapping diagonal lines).
A k-d tree algorithm or an R-tree could be used, but in general a linear brute force search will probably just work fine for most applications.
Sample brute force solution algorithm
private Node findNearestNode(ObservableList<Node> nodes, double x, double y) {
Point2D pClick = new Point2D(x, y);
Node nearestNode = null;
double closestDistance = Double.POSITIVE_INFINITY;
for (Node node : nodes) {
Bounds bounds = node.getBoundsInParent();
Point2D[] corners = new Point2D[] {
new Point2D(bounds.getMinX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMaxY()),
new Point2D(bounds.getMinX(), bounds.getMaxY()),
};
for (Point2D pCompare: corners) {
double nextDist = pClick.distance(pCompare);
if (nextDist < closestDistance) {
closestDistance = nextDist;
nearestNode = node;
}
}
}
return nearestNode;
}
Executable Solution
import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.geometry.*;
import javafx.scene.*;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import java.io.IOException;
import java.net.*;
import java.util.Random;
public class FindNearest extends Application {
private static final int N_SHAPES = 10;
private static final double W = 600, H = 400;
private ShapeMachine machine;
public static void main(String[] args) {
launch(args);
}
#Override
public void init() throws MalformedURLException, URISyntaxException {
double maxShapeSize = W / 8;
double minShapeSize = maxShapeSize / 2;
machine = new ShapeMachine(W, H, maxShapeSize, minShapeSize);
}
#Override
public void start(final Stage stage) throws IOException, URISyntaxException {
Pane pane = new Pane();
pane.setPrefSize(W, H);
for (int i = 0; i < N_SHAPES; i++) {
pane.getChildren().add(machine.randomShape());
}
pane.setOnMouseClicked(event -> {
Node node = findNearestNode(pane.getChildren(), event.getX(), event.getY());
highlightSelected(node, pane.getChildren());
});
Scene scene = new Scene(pane);
configureExitOnAnyKey(stage, scene);
stage.setScene(scene);
stage.setResizable(false);
stage.show();
}
private void highlightSelected(Node selected, ObservableList<Node> children) {
for (Node node: children) {
node.setEffect(null);
}
if (selected != null) {
selected.setEffect(new DropShadow(10, Color.YELLOW));
}
}
private Node findNearestNode(ObservableList<Node> nodes, double x, double y) {
Point2D pClick = new Point2D(x, y);
Node nearestNode = null;
double closestDistance = Double.POSITIVE_INFINITY;
for (Node node : nodes) {
Bounds bounds = node.getBoundsInParent();
Point2D[] corners = new Point2D[] {
new Point2D(bounds.getMinX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMaxY()),
new Point2D(bounds.getMinX(), bounds.getMaxY()),
};
for (Point2D pCompare: corners) {
double nextDist = pClick.distance(pCompare);
if (nextDist < closestDistance) {
closestDistance = nextDist;
nearestNode = node;
}
}
}
return nearestNode;
}
private void configureExitOnAnyKey(final Stage stage, Scene scene) {
scene.setOnKeyPressed(keyEvent -> stage.hide());
}
}
Auxiliary random shape generation class
This class is not key to the solution, it just generates some shapes for testing.
class ShapeMachine {
private static final Random random = new Random();
private final double canvasWidth, canvasHeight, maxShapeSize, minShapeSize;
ShapeMachine(double canvasWidth, double canvasHeight, double maxShapeSize, double minShapeSize) {
this.canvasWidth = canvasWidth;
this.canvasHeight = canvasHeight;
this.maxShapeSize = maxShapeSize;
this.minShapeSize = minShapeSize;
}
private Color randomColor() {
return Color.rgb(random.nextInt(256), random.nextInt(256), random.nextInt(256), 0.1 + random.nextDouble() * 0.9);
}
enum Shapes {Circle, Rectangle, Line}
public Shape randomShape() {
Shape shape = null;
switch (Shapes.values()[random.nextInt(Shapes.values().length)]) {
case Circle:
shape = randomCircle();
break;
case Rectangle:
shape = randomRectangle();
break;
case Line:
shape = randomLine();
break;
default:
System.out.println("Unknown Shape");
System.exit(1);
}
Color fill = randomColor();
shape.setFill(fill);
shape.setStroke(deriveStroke(fill));
shape.setStrokeWidth(deriveStrokeWidth(shape));
shape.setStrokeLineCap(StrokeLineCap.ROUND);
shape.relocate(randomShapeX(), randomShapeY());
return shape;
}
private double deriveStrokeWidth(Shape shape) {
return Math.max(shape.getLayoutBounds().getWidth() / 10, shape.getLayoutBounds().getHeight() / 10);
}
private Color deriveStroke(Color fill) {
return fill.desaturate();
}
private double randomShapeSize() {
double range = maxShapeSize - minShapeSize;
return random.nextDouble() * range + minShapeSize;
}
private double randomShapeX() {
return random.nextDouble() * (canvasWidth + maxShapeSize) - maxShapeSize / 2;
}
private double randomShapeY() {
return random.nextDouble() * (canvasHeight + maxShapeSize) - maxShapeSize / 2;
}
private Shape randomLine() {
int xZero = random.nextBoolean() ? 1 : 0;
int yZero = random.nextBoolean() || xZero == 0 ? 1 : 0;
int xSign = random.nextBoolean() ? 1 : -1;
int ySign = random.nextBoolean() ? 1 : -1;
return new Line(0, 0, xZero * xSign * randomShapeSize(), yZero * ySign * randomShapeSize());
}
private Shape randomRectangle() {
return new Rectangle(0, 0, randomShapeSize(), randomShapeSize());
}
private Shape randomCircle() {
double radius = randomShapeSize() / 2;
return new Circle(radius, radius, radius);
}
}
Further example placing objects in a zoomable/scrollable area
This solution uses the nearest node solution code from above and combines it with the zoomed node in a ScrollPane code from: JavaFX correct scaling. The purpose is to demonstrate that the choosing algorithm works even on nodes which have had a scaling transform applied to them (because it is based upon boundsInParent). The code is just meant as a proof of concept and not as a stylistic sample of how to structure the functionality into a class domain model :-)
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.*;
import javafx.collections.ObservableList;
import javafx.event.*;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
public class GraphicsScalingApp extends Application {
private static final int N_SHAPES = 10;
private static final double W = 600, H = 400;
private ShapeMachine machine;
public static void main(String[] args) {
launch(args);
}
#Override
public void init() throws MalformedURLException, URISyntaxException {
double maxShapeSize = W / 8;
double minShapeSize = maxShapeSize / 2;
machine = new ShapeMachine(W, H, maxShapeSize, minShapeSize);
}
#Override
public void start(final Stage stage) {
Pane pane = new Pane();
pane.setPrefSize(W, H);
for (int i = 0; i < N_SHAPES; i++) {
pane.getChildren().add(machine.randomShape());
}
pane.setOnMouseClicked(event -> {
Node node = findNearestNode(pane.getChildren(), event.getX(), event.getY());
System.out.println("Found: " + node + " at " + event.getX() + "," + event.getY());
highlightSelected(node, pane.getChildren());
});
final Group group = new Group(
pane
);
Parent zoomPane = createZoomPane(group);
VBox layout = new VBox();
layout.getChildren().setAll(createMenuBar(stage, group), zoomPane);
VBox.setVgrow(zoomPane, Priority.ALWAYS);
Scene scene = new Scene(layout);
stage.setTitle("Zoomy");
stage.getIcons().setAll(new Image(APP_ICON));
stage.setScene(scene);
stage.show();
}
private Parent createZoomPane(final Group group) {
final double SCALE_DELTA = 1.1;
final StackPane zoomPane = new StackPane();
zoomPane.getChildren().add(group);
final ScrollPane scroller = new ScrollPane();
final Group scrollContent = new Group(zoomPane);
scroller.setContent(scrollContent);
scroller.viewportBoundsProperty().addListener(new ChangeListener<Bounds>() {
#Override
public void changed(ObservableValue<? extends Bounds> observable,
Bounds oldValue, Bounds newValue) {
zoomPane.setMinSize(newValue.getWidth(), newValue.getHeight());
}
});
scroller.setPrefViewportWidth(256);
scroller.setPrefViewportHeight(256);
zoomPane.setOnScroll(new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
event.consume();
if (event.getDeltaY() == 0) {
return;
}
double scaleFactor = (event.getDeltaY() > 0) ? SCALE_DELTA
: 1 / SCALE_DELTA;
// amount of scrolling in each direction in scrollContent coordinate
// units
Point2D scrollOffset = figureScrollOffset(scrollContent, scroller);
group.setScaleX(group.getScaleX() * scaleFactor);
group.setScaleY(group.getScaleY() * scaleFactor);
// move viewport so that old center remains in the center after the
// scaling
repositionScroller(scrollContent, scroller, scaleFactor, scrollOffset);
}
});
// Panning via drag....
final ObjectProperty<Point2D> lastMouseCoordinates = new SimpleObjectProperty<Point2D>();
scrollContent.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
lastMouseCoordinates.set(new Point2D(event.getX(), event.getY()));
}
});
scrollContent.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
double deltaX = event.getX() - lastMouseCoordinates.get().getX();
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
double deltaH = deltaX * (scroller.getHmax() - scroller.getHmin()) / extraWidth;
double desiredH = scroller.getHvalue() - deltaH;
scroller.setHvalue(Math.max(0, Math.min(scroller.getHmax(), desiredH)));
double deltaY = event.getY() - lastMouseCoordinates.get().getY();
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
double deltaV = deltaY * (scroller.getHmax() - scroller.getHmin()) / extraHeight;
double desiredV = scroller.getVvalue() - deltaV;
scroller.setVvalue(Math.max(0, Math.min(scroller.getVmax(), desiredV)));
}
});
return scroller;
}
private Point2D figureScrollOffset(Node scrollContent, ScrollPane scroller) {
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
double hScrollProportion = (scroller.getHvalue() - scroller.getHmin()) / (scroller.getHmax() - scroller.getHmin());
double scrollXOffset = hScrollProportion * Math.max(0, extraWidth);
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
double vScrollProportion = (scroller.getVvalue() - scroller.getVmin()) / (scroller.getVmax() - scroller.getVmin());
double scrollYOffset = vScrollProportion * Math.max(0, extraHeight);
return new Point2D(scrollXOffset, scrollYOffset);
}
private void repositionScroller(Node scrollContent, ScrollPane scroller, double scaleFactor, Point2D scrollOffset) {
double scrollXOffset = scrollOffset.getX();
double scrollYOffset = scrollOffset.getY();
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
if (extraWidth > 0) {
double halfWidth = scroller.getViewportBounds().getWidth() / 2;
double newScrollXOffset = (scaleFactor - 1) * halfWidth + scaleFactor * scrollXOffset;
scroller.setHvalue(scroller.getHmin() + newScrollXOffset * (scroller.getHmax() - scroller.getHmin()) / extraWidth);
} else {
scroller.setHvalue(scroller.getHmin());
}
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
if (extraHeight > 0) {
double halfHeight = scroller.getViewportBounds().getHeight() / 2;
double newScrollYOffset = (scaleFactor - 1) * halfHeight + scaleFactor * scrollYOffset;
scroller.setVvalue(scroller.getVmin() + newScrollYOffset * (scroller.getVmax() - scroller.getVmin()) / extraHeight);
} else {
scroller.setHvalue(scroller.getHmin());
}
}
private SVGPath createCurve() {
SVGPath ellipticalArc = new SVGPath();
ellipticalArc.setContent("M10,150 A15 15 180 0 1 70 140 A15 25 180 0 0 130 130 A15 55 180 0 1 190 120");
ellipticalArc.setStroke(Color.LIGHTGREEN);
ellipticalArc.setStrokeWidth(4);
ellipticalArc.setFill(null);
return ellipticalArc;
}
private SVGPath createStar() {
SVGPath star = new SVGPath();
star.setContent("M100,10 L100,10 40,180 190,60 10,60 160,180 z");
star.setStrokeLineJoin(StrokeLineJoin.ROUND);
star.setStroke(Color.BLUE);
star.setFill(Color.DARKBLUE);
star.setStrokeWidth(4);
return star;
}
private MenuBar createMenuBar(final Stage stage, final Group group) {
Menu fileMenu = new Menu("_File");
MenuItem exitMenuItem = new MenuItem("E_xit");
exitMenuItem.setGraphic(new ImageView(new Image(CLOSE_ICON)));
exitMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
stage.close();
}
});
fileMenu.getItems().setAll(exitMenuItem);
Menu zoomMenu = new Menu("_Zoom");
MenuItem zoomResetMenuItem = new MenuItem("Zoom _Reset");
zoomResetMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.ESCAPE));
zoomResetMenuItem.setGraphic(new ImageView(new Image(ZOOM_RESET_ICON)));
zoomResetMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
group.setScaleX(1);
group.setScaleY(1);
}
});
MenuItem zoomInMenuItem = new MenuItem("Zoom _In");
zoomInMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.I));
zoomInMenuItem.setGraphic(new ImageView(new Image(ZOOM_IN_ICON)));
zoomInMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
group.setScaleX(group.getScaleX() * 1.5);
group.setScaleY(group.getScaleY() * 1.5);
}
});
MenuItem zoomOutMenuItem = new MenuItem("Zoom _Out");
zoomOutMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.O));
zoomOutMenuItem.setGraphic(new ImageView(new Image(ZOOM_OUT_ICON)));
zoomOutMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
group.setScaleX(group.getScaleX() * 1 / 1.5);
group.setScaleY(group.getScaleY() * 1 / 1.5);
}
});
zoomMenu.getItems().setAll(zoomResetMenuItem, zoomInMenuItem,
zoomOutMenuItem);
MenuBar menuBar = new MenuBar();
menuBar.getMenus().setAll(fileMenu, zoomMenu);
return menuBar;
}
private void highlightSelected(Node selected, ObservableList<Node> children) {
for (Node node : children) {
node.setEffect(null);
}
if (selected != null) {
selected.setEffect(new DropShadow(10, Color.YELLOW));
}
}
private Node findNearestNode(ObservableList<Node> nodes, double x, double y) {
Point2D pClick = new Point2D(x, y);
Node nearestNode = null;
double closestDistance = Double.POSITIVE_INFINITY;
for (Node node : nodes) {
Bounds bounds = node.getBoundsInParent();
Point2D[] corners = new Point2D[]{
new Point2D(bounds.getMinX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMaxY()),
new Point2D(bounds.getMinX(), bounds.getMaxY()),
};
for (Point2D pCompare : corners) {
double nextDist = pClick.distance(pCompare);
if (nextDist < closestDistance) {
closestDistance = nextDist;
nearestNode = node;
}
}
}
return nearestNode;
}
// icons source from:
// http://www.iconarchive.com/show/soft-scraps-icons-by-deleket.html
// icon license: CC Attribution-Noncommercial-No Derivate 3.0 =?
// http://creativecommons.org/licenses/by-nc-nd/3.0/
// icon Commercial usage: Allowed (Author Approval required -> Visit artist
// website for details).
public static final String APP_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/128/Zoom-icon.png";
public static final String ZOOM_RESET_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-icon.png";
public static final String ZOOM_OUT_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-Out-icon.png";
public static final String ZOOM_IN_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-In-icon.png";
public static final String CLOSE_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Button-Close-icon.png";
}

Bidirectional property binding via expression or some approach to work around it

I am relatively new to property bindings and I am looking for some high-level advice on how to approach a design problem, which I will try to describe a simple example of here.
Problem description
The goal in this example is to allow the user to specify a box/rectangular region interactively in a pannable and zoomable 2D space. The 2D screen-space in which the box is depicted, maps to a 2D "real-space" (e.g. voltage vs time cartesian space, or GPS, or whatever). The user should be able to zoom/pan his viewport vertically/horizontally at any time, thereby changing the mapping between these two spaces.
screen-space <-------- user-adjustable mapping --------> real-space
The user specifies the rectangle in his viewport by dragging borders/corners, as in this demo:
class InteractiveHandle extends Rectangle {
private final Cursor hoverCursor;
private final Cursor activeCursor;
private final DoubleProperty centerXProperty = new SimpleDoubleProperty();
private final DoubleProperty centerYProperty = new SimpleDoubleProperty();
InteractiveHandle(DoubleProperty x, DoubleProperty y, double w, double h) {
super();
centerXProperty.bindBidirectional(x);
centerYProperty.bindBidirectional(y);
widthProperty().set(w);
heightProperty().set(h);
hoverCursor = Cursor.MOVE;
activeCursor = Cursor.MOVE;
bindRect();
enableDrag(true,true);
}
InteractiveHandle(DoubleProperty x, ObservableDoubleValue y, double w, ObservableDoubleValue h) {
super();
centerXProperty.bindBidirectional(x);
centerYProperty.bind(y);
widthProperty().set(w);
heightProperty().bind(h);
hoverCursor = Cursor.H_RESIZE;
activeCursor = Cursor.H_RESIZE;
bindRect();
enableDrag(true,false);
}
InteractiveHandle(ObservableDoubleValue x, DoubleProperty y, ObservableDoubleValue w, double h) {
super();
centerXProperty.bind(x);
centerYProperty.bindBidirectional(y);
widthProperty().bind(w);
heightProperty().set(h);
hoverCursor = Cursor.V_RESIZE;
activeCursor = Cursor.V_RESIZE;
bindRect();
enableDrag(false,true);
}
InteractiveHandle(ObservableDoubleValue x, ObservableDoubleValue y, ObservableDoubleValue w, ObservableDoubleValue h) {
super();
centerXProperty.bind(x);
centerYProperty.bind(y);
widthProperty().bind(w);
heightProperty().bind(h);
hoverCursor = Cursor.DEFAULT;
activeCursor = Cursor.DEFAULT;
bindRect();
enableDrag(false,false);
}
private void bindRect(){
xProperty().bind(centerXProperty.subtract(widthProperty().divide(2)));
yProperty().bind(centerYProperty.subtract(heightProperty().divide(2)));
}
//make a node movable by dragging it around with the mouse.
private void enableDrag(boolean xDraggable, boolean yDraggable) {
final Delta dragDelta = new Delta();
setOnMousePressed((MouseEvent mouseEvent) -> {
// record a delta distance for the drag and drop operation.
dragDelta.x = centerXProperty.get() - mouseEvent.getX();
dragDelta.y = centerYProperty.get() - mouseEvent.getY();
getScene().setCursor(activeCursor);
});
setOnMouseReleased((MouseEvent mouseEvent) -> {
getScene().setCursor(hoverCursor);
});
setOnMouseDragged((MouseEvent mouseEvent) -> {
if(xDraggable){
double newX = mouseEvent.getX() + dragDelta.x;
if (newX > 0 && newX < getScene().getWidth()) {
centerXProperty.set(newX);
}
}
if(yDraggable){
double newY = mouseEvent.getY() + dragDelta.y;
if (newY > 0 && newY < getScene().getHeight()) {
centerYProperty.set(newY);
}
}
});
setOnMouseEntered((MouseEvent mouseEvent) -> {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(hoverCursor);
}
});
setOnMouseExited((MouseEvent mouseEvent) -> {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.DEFAULT);
}
});
}
//records relative x and y co-ordinates.
private class Delta { double x, y; }
}
public class InteractiveBox extends Group {
private static final double sideHandleWidth = 2;
private static final double cornerHandleSize = 4;
private static final double minHandleFraction = 0.5;
private static final double maxCornerClearance = 6;
private static final double handleInset = 2;
private final Rectangle rectangle;
private final InteractiveHandle ihLeft;
private final InteractiveHandle ihTop;
private final InteractiveHandle ihRight;
private final InteractiveHandle ihBottom;
private final InteractiveHandle ihTopLeft;
private final InteractiveHandle ihTopRight;
private final InteractiveHandle ihBottomLeft;
private final InteractiveHandle ihBottomRight;
InteractiveBox(DoubleProperty xMin, DoubleProperty yMin, DoubleProperty xMax, DoubleProperty yMax){
super();
rectangle = new Rectangle();
rectangle.widthProperty().bind(xMax.subtract(xMin));
rectangle.heightProperty().bind(yMax.subtract(yMin));
rectangle.xProperty().bind(xMin);
rectangle.yProperty().bind(yMin);
DoubleBinding xMid = xMin.add(xMax).divide(2);
DoubleBinding yMid = yMin.add(yMax).divide(2);
DoubleBinding hx = (DoubleBinding) Bindings.max(
rectangle.widthProperty().multiply(minHandleFraction)
,rectangle.widthProperty().subtract(maxCornerClearance*2)
);
DoubleBinding vx = (DoubleBinding) Bindings.max(
rectangle.heightProperty().multiply(minHandleFraction)
,rectangle.heightProperty().subtract(maxCornerClearance*2)
);
ihTopLeft = new InteractiveHandle(xMin,yMax,cornerHandleSize,cornerHandleSize);
ihTopRight = new InteractiveHandle(xMax,yMax,cornerHandleSize,cornerHandleSize);
ihBottomLeft = new InteractiveHandle(xMin,yMin,cornerHandleSize,cornerHandleSize);
ihBottomRight = new InteractiveHandle(xMax,yMin,cornerHandleSize,cornerHandleSize);
ihLeft = new InteractiveHandle(xMin,yMid,sideHandleWidth,vx);
ihTop = new InteractiveHandle(xMid,yMax,hx,sideHandleWidth);
ihRight = new InteractiveHandle(xMax,yMid,sideHandleWidth,vx);
ihBottom = new InteractiveHandle(xMid,yMin,hx,sideHandleWidth);
style(ihLeft);
style(ihTop);
style(ihRight);
style(ihBottom);
style(ihTopLeft);
style(ihTopRight);
style(ihBottomLeft);
style(ihBottomRight);
getChildren().addAll(rectangle
,ihTopLeft, ihTopRight, ihBottomLeft, ihBottomRight
,ihLeft, ihTop, ihRight, ihBottom
);
rectangle.setFill(Color.ALICEBLUE);
rectangle.setStroke(Color.LIGHTGRAY);
rectangle.setStrokeWidth(2);
rectangle.setStrokeType(StrokeType.CENTERED);
}
private void style(InteractiveHandle ih){
ih.setStroke(Color.TRANSPARENT);
ih.setStrokeWidth(handleInset);
ih.setStrokeType(StrokeType.OUTSIDE);
}
}
public class Summoner extends Application {
DoubleProperty x = new SimpleDoubleProperty(50);
DoubleProperty y = new SimpleDoubleProperty(50);
DoubleProperty xMax = new SimpleDoubleProperty(100);
DoubleProperty yMax = new SimpleDoubleProperty(100);
#Override
public void start(Stage primaryStage) {
InteractiveBox box = new InteractiveBox(x,y,xMax,yMax);
Pane root = new Pane();
root.getChildren().add(box);
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
After the rectangle has been specified by the user, its coordinates (in real-space) are passed on to or read by a different part of the program.
My rationale
My first instinct was to use the built-in scale/translate properties in JavaFX nodes to implement the mapping, but we want borders and handles to have a consistent size/appearance regardless of zoom-state; zooming should only embiggen the conceptual rectangle itself, not thicken the borders or corner-handles.
(In the following, arrows represent causality/influence/dependency. For example, A ---> B could mean property B is bound to property A (or it could mean that event-handler A sets property B), and <-----> could represent a bidirectional binding. A multi-tailed arrow such as --+--> could represent a binding that depends on multiple input observables.)
So my question became: which of the following should I do?
real-space-properties ---+--> screen-space-properties
real-space-properties <--+--- screen-space properties
or something different, using <---->
On the one hand, we have mouse events and the rendered rectangle itself in screen-space. This argues for a self-contained interactive rectangle (whose screen-space position/dimension properties we can observe (as well as manipulate, if we wanted to) externally) as per the demo above.
mouse events -----> screen-space properties ------> depicted rectangle
|
|
--------> real-space properties -----> API
On the other hand, when the user adjusts pan/zoom, we want the rectangle's properties in real-space (not screen-space) to be preserved. This argues for binding the screen-space properties to real-space properties using pan&zoom-state properties:
pan/zoom properties
|
|
real-space properties ---+--> screen-space properties ------> depicted rectangle
|
|
-------> API
If I try to put together both approaches above, I run into a problem:
mouse events
|
pan/zoom properties |
| |
| v
real-space properties <--+--> screen-space properties ------> depicted rectangle
| *
|
-------> API
This diagram makes a lot of sense to me, but I don't think the kind of "bidirectional" 3-way binding at * is possible, directly. But is there perhaps a simple way to emulate/work around it? Or should I take an entirely different approach?
Here's an example of a rectangle on a zoom & pannable pane with a constant stroke width. You just have to define the scale factor as a property of the pane, bind a it to a property in the calling class and divide that into a property that's bound to the strokewidth of the rectangle.
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ZoomAndPanExample extends Application {
private ScrollPane scrollPane = new ScrollPane();
private final DoubleProperty zoomProperty = new SimpleDoubleProperty(1.0d);
private final DoubleProperty strokeWidthProperty = new SimpleDoubleProperty(1.0d);
private final DoubleProperty deltaY = new SimpleDoubleProperty(0.0d);
private final Group group = new Group();
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) {
scrollPane.setPannable(true);
scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);
AnchorPane.setTopAnchor(scrollPane, 10.0d);
AnchorPane.setRightAnchor(scrollPane, 10.0d);
AnchorPane.setBottomAnchor(scrollPane, 10.0d);
AnchorPane.setLeftAnchor(scrollPane, 10.0d);
AnchorPane root = new AnchorPane();
Rectangle rect = new Rectangle(80, 60);
rect.setStroke(Color.NAVY);
rect.setFill(Color.web("#000080", 0.2));
rect.setStrokeType(StrokeType.INSIDE);
rect.strokeWidthProperty().bind(strokeWidthProperty.divide(zoomProperty));
group.getChildren().add(rect);
// create canvas
PanAndZoomPane panAndZoomPane = new PanAndZoomPane();
zoomProperty.bind(panAndZoomPane.myScale);
deltaY.bind(panAndZoomPane.deltaY);
panAndZoomPane.getChildren().add(group);
SceneGestures sceneGestures = new SceneGestures(panAndZoomPane);
scrollPane.setContent(panAndZoomPane);
panAndZoomPane.toBack();
scrollPane.addEventFilter( MouseEvent.MOUSE_CLICKED, sceneGestures.getOnMouseClickedEventHandler());
scrollPane.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scrollPane.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scrollPane.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
root.getChildren().add(scrollPane);
Scene scene = new Scene(root, 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
class PanAndZoomPane extends Pane {
public static final double DEFAULT_DELTA = 1.3d;
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public DoubleProperty deltaY = new SimpleDoubleProperty(0.0);
private Timeline timeline;
public PanAndZoomPane() {
this.timeline = new Timeline(60);
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
}
public double getScale() {
return myScale.get();
}
public void setScale( double scale) {
myScale.set(scale);
}
public void setPivot( double x, double y, double scale) {
// note: pivot value must be untransformed, i. e. without scaling
// timeline that scales and moves the node
timeline.getKeyFrames().clear();
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.millis(200), new KeyValue(translateXProperty(), getTranslateX() - x)),
new KeyFrame(Duration.millis(200), new KeyValue(translateYProperty(), getTranslateY() - y)),
new KeyFrame(Duration.millis(200), new KeyValue(myScale, scale))
);
timeline.play();
}
/**
* fit the rectangle to the width of the window
*/
public void fitWidth () {
double scale = getParent().getLayoutBounds().getMaxX()/getLayoutBounds().getMaxX();
double oldScale = getScale();
double f = (scale / oldScale)-1;
double dx = getTranslateX() - getBoundsInParent().getMinX() - getBoundsInParent().getWidth()/2;
double dy = getTranslateY() - getBoundsInParent().getMinY() - getBoundsInParent().getHeight()/2;
double newX = f*dx + getBoundsInParent().getMinX();
double newY = f*dy + getBoundsInParent().getMinY();
setPivot(newX, newY, scale);
}
public void resetZoom () {
double scale = 1.0d;
double x = getTranslateX();
double y = getTranslateY();
setPivot(x, y, scale);
}
public double getDeltaY() {
return deltaY.get();
}
public void setDeltaY( double dY) {
deltaY.set(dY);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
public class SceneGestures {
private DragContext sceneDragContext = new DragContext();
PanAndZoomPane panAndZoomPane;
public SceneGestures( PanAndZoomPane canvas) {
this.panAndZoomPane = canvas;
}
public EventHandler<MouseEvent> getOnMouseClickedEventHandler() {
return onMouseClickedEventHandler;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
public EventHandler<ScrollEvent> getOnScrollEventHandler() {
return onScrollEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
sceneDragContext.mouseAnchorX = event.getX();
sceneDragContext.mouseAnchorY = event.getY();
sceneDragContext.translateAnchorX = panAndZoomPane.getTranslateX();
sceneDragContext.translateAnchorY = panAndZoomPane.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
panAndZoomPane.setTranslateX(sceneDragContext.translateAnchorX + event.getX() - sceneDragContext.mouseAnchorX);
panAndZoomPane.setTranslateY(sceneDragContext.translateAnchorY + event.getY() - sceneDragContext.mouseAnchorY);
event.consume();
}
};
/**
* Mouse wheel handler: zoom to pivot point
*/
private EventHandler<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double delta = PanAndZoomPane.DEFAULT_DELTA;
double scale = panAndZoomPane.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
panAndZoomPane.setDeltaY(event.getDeltaY());
if (panAndZoomPane.deltaY.get() < 0) {
scale /= delta;
} else {
scale *= delta;
}
double f = (scale / oldScale)-1;
double dx = (event.getX() - (panAndZoomPane.getBoundsInParent().getWidth()/2 + panAndZoomPane.getBoundsInParent().getMinX()));
double dy = (event.getY() - (panAndZoomPane.getBoundsInParent().getHeight()/2 + panAndZoomPane.getBoundsInParent().getMinY()));
panAndZoomPane.setPivot(f*dx, f*dy, scale);
event.consume();
}
};
/**
* Mouse click handler
*/
private EventHandler<MouseEvent> onMouseClickedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.getButton().equals(MouseButton.PRIMARY)) {
if (event.getClickCount() == 2) {
panAndZoomPane.resetZoom();
}
}
if (event.getButton().equals(MouseButton.SECONDARY)) {
if (event.getClickCount() == 2) {
panAndZoomPane.fitWidth();
}
}
}
};
}
}

Automatically resize Canvas to fill the enclosing Parent

I recently wanted to create an animated background in JavaFX, similar to the Swing example seen here. I used a Canvas on which to draw, as shown in Working with the Canvas API, and an AnimationTimer for the drawing loop, as shown in Animation Basics. Unfortunately, I'm not sure how to resize the Canvas automatically as the enclosing Stage is resized. What is a good approach?
A similar question is examined in How to make canvas Resizable in javaFX?, but the accepted answer there lacks the binding illustrated in the accepted answer here.
In the example below, the static nested class CanvasPane wraps an instance of Canvas in a Pane and overrides layoutChildren() to make the canvas dimensions match the enclosing Pane. Note that Canvas returns false from isResizable(), so "the parent cannot resize it during layout," and Pane "does not perform layout beyond resizing resizable children to their preferred sizes." The width and height used to construct the canvas become its initial size. A similar approach is used in the Ensemble particle simulation, FireworksApp, to scale a background image while retaining its aspect ratio.
As an aside, note the difference from using fully saturated colors compared to the original. These related examples illustrate placing controls atop the animated background.
import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/**
* #see https://stackoverflow.com/a/31761362/230513
* #see https://stackoverflow.com/a/8616169/230513
*/
public class Baubles extends Application {
private static final int MAX = 64;
private static final double WIDTH = 640;
private static final double HEIGHT = 480;
private static final Random RND = new Random();
private final Queue<Bauble> queue = new LinkedList<>();
private Canvas canvas;
#Override
public void start(Stage stage) {
CanvasPane canvasPane = new CanvasPane(WIDTH, HEIGHT);
canvas = canvasPane.getCanvas();
BorderPane root = new BorderPane(canvasPane);
CheckBox cb = new CheckBox("Animate");
cb.setSelected(true);
root.setBottom(cb);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
for (int i = 0; i < MAX; i++) {
queue.add(randomBauble());
}
AnimationTimer loop = new AnimationTimer() {
#Override
public void handle(long now) {
GraphicsContext g = canvas.getGraphicsContext2D();
g.setFill(Color.BLACK);
g.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
for (Bauble b : queue) {
g.setFill(b.c);
g.fillOval(b.x, b.y, b.d, b.d);
}
queue.add(randomBauble());
queue.remove();
}
};
loop.start();
cb.selectedProperty().addListener((Observable o) -> {
if (cb.isSelected()) {
loop.start();
} else {
loop.stop();
}
});
}
private static class Bauble {
private final double x, y, d;
private final Color c;
public Bauble(double x, double y, double r, Color c) {
this.x = x - r;
this.y = y - r;
this.d = 2 * r;
this.c = c;
}
}
private Bauble randomBauble() {
double x = RND.nextDouble() * canvas.getWidth();
double y = RND.nextDouble() * canvas.getHeight();
double r = RND.nextDouble() * MAX + MAX / 2;
Color c = Color.hsb(RND.nextDouble() * 360, 1, 1, 0.75);
return new Bauble(x, y, r, c);
}
private static class CanvasPane extends Pane {
private final Canvas canvas;
public CanvasPane(double width, double height) {
canvas = new Canvas(width, height);
getChildren().add(canvas);
}
public Canvas getCanvas() {
return canvas;
}
#Override
protected void layoutChildren() {
super.layoutChildren();
final double x = snappedLeftInset();
final double y = snappedTopInset();
// Java 9 - snapSize is deprecated, use snapSizeX() and snapSizeY() accordingly
final double w = snapSize(getWidth()) - x - snappedRightInset();
final double h = snapSize(getHeight()) - y - snappedBottomInset();
canvas.setLayoutX(x);
canvas.setLayoutY(y);
canvas.setWidth(w);
canvas.setHeight(h);
}
}
public static void main(String[] args) {
launch(args);
}
}
I combined both prior solutions ( #trashgod and #clataq's ) by putting the canvas in a Pane and binding it to it:
private static class CanvasPane extends Pane {
final Canvas canvas;
CanvasPane(double width, double height) {
setWidth(width);
setHeight(height);
canvas = new Canvas(width, height);
getChildren().add(canvas);
canvas.widthProperty().bind(this.widthProperty());
canvas.heightProperty().bind(this.heightProperty());
}
}
Couldn't you do this with a Binding as well? The following seems to produce the same results without having to add the derived class.
import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.DoubleBinding;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/**
* #see http://stackoverflow.com/a/31761362/230513
* #see http://stackoverflow.com/a/8616169/230513
*/
public class Baubles extends Application {
private static final int MAX = 64;
private static final double WIDTH = 640;
private static final double HEIGHT = 480;
private static final Random RND = new Random();
private final Queue<Bauble> queue = new LinkedList<>();
private Canvas canvas;
#Override
public void start(Stage stage) {
canvas = new Canvas(WIDTH, HEIGHT);
BorderPane root = new BorderPane(canvas);
CheckBox cb = new CheckBox("Animate");
cb.setSelected(true);
root.setBottom(cb);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
// Create bindings for resizing.
DoubleBinding heightBinding = root.heightProperty()
.subtract(root.bottomProperty().getValue().getBoundsInParent().getHeight());
canvas.widthProperty().bind(root.widthProperty());
canvas.heightProperty().bind(heightBinding);
for (int i = 0; i < MAX; i++) {
queue.add(randomBauble());
}
AnimationTimer loop = new AnimationTimer() {
#Override
public void handle(long now) {
GraphicsContext g = canvas.getGraphicsContext2D();
g.setFill(Color.BLACK);
g.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
for (Bauble b : queue) {
g.setFill(b.c);
g.fillOval(b.x, b.y, b.d, b.d);
}
queue.add(randomBauble());
queue.remove();
}
};
loop.start();
cb.selectedProperty().addListener((Observable o) -> {
if (cb.isSelected()) {
loop.start();
} else {
loop.stop();
}
});
}
private static class Bauble {
private final double x, y, d;
private final Color c;
public Bauble(double x, double y, double r, Color c) {
this.x = x - r;
this.y = y - r;
this.d = 2 * r;
this.c = c;
}
}
private Bauble randomBauble() {
double x = RND.nextDouble() * canvas.getWidth();
double y = RND.nextDouble() * canvas.getHeight();
double r = RND.nextDouble() * MAX + MAX / 2;
Color c = Color.hsb(RND.nextDouble() * 360, 1, 1, 0.75);
return new Bauble(x, y, r, c);
}
public static void main(String[] args) {
launch(args);
}
}

JavaFX 3D rotations

My problems are:
the below code is based on the "MolecularSampleApp" from the oracle tutorial site, but very much simplified. It only shows a box and a red sphere for orientation purpose. The sequence of rotations is around x-axis then y-axis and finally z-axis. Following rotations are apparently done in coordinate-axes that are rotated with the rotations before.
(I checked that again and again with a cube and sequences of 90° rotations) So, when the user rotates the camera view with the mouse, this is very unintuitive, because the rotation behaviour changes after rotating around the vertical screen axis (because the horizontal axis will then be rotated too).
Try it with my code below, or with the MolecularSampleApp - its the same unnatural feeling. Is there an easy way to get over this?
But what I don't even understand is the behaviour when the mousePressed-code is performed: Here, the camera is always rotated in a FIXED system! The axes are NOT rotated with the camera, although its essentially the same code (except the rotation angle is of course not accumulated here). Anybody knows how is this possible?
package trafotest;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.scene.DepthTest;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
public class TrafoTest extends Application {
final Group root = new Group();
final XformWorld world = new XformWorld();
final PerspectiveCamera camera = new PerspectiveCamera(true);
final XformCamera cameraXform = new XformCamera();
private static final double CAMERA_INITIAL_DISTANCE = -1000;
private static final double CAMERA_NEAR_CLIP = 0.1;
private static final double CAMERA_FAR_CLIP = 10000.0;
double mousePosX, mousePosY, mouseOldX, mouseOldY, mouseDeltaX, mouseDeltaY;
#Override
public void start(Stage primaryStage) {
root.getChildren().add(world);
root.setDepthTest(DepthTest.ENABLE);
buildCamera();
buildBodySystem();
Scene scene = new Scene(root, 800, 600, true);
scene.setFill(Color.GREY);
handleMouse(scene);
primaryStage.setTitle("Transformationen");
primaryStage.setScene(scene);
primaryStage.show();
scene.setCamera(camera);
}
private void buildCamera() {
root.getChildren().add(cameraXform);
cameraXform.getChildren().add(camera);
camera.setNearClip(CAMERA_NEAR_CLIP);
camera.setFarClip(CAMERA_FAR_CLIP);
camera.setTranslateZ(CAMERA_INITIAL_DISTANCE);
}
private void buildBodySystem() {
PhongMaterial whiteMaterial = new PhongMaterial();
whiteMaterial.setDiffuseColor(Color.WHITE);
whiteMaterial.setSpecularColor(Color.LIGHTBLUE);
Box box = new Box(400, 200, 100);
box.setMaterial(whiteMaterial);
PhongMaterial redMaterial = new PhongMaterial();
redMaterial.setDiffuseColor(Color.DARKRED);
redMaterial.setSpecularColor(Color.RED);
Sphere sphere = new Sphere(5);
sphere.setMaterial(redMaterial);
sphere.setTranslateZ(-50.0);
world.getChildren().addAll(box);
world.getChildren().addAll(sphere);
}
private void handleMouse(Scene scene) {
scene.setOnMousePressed((MouseEvent me) -> {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
// this is done after clicking and the rotations are apearently
// performed in coordinates that are NOT rotated with the camera.
// (pls activate the two lines below for clicking)
//cameraXform.rx.setAngle(-90.0);
//cameraXform.ry.setAngle(180.0);
});
scene.setOnMouseDragged((MouseEvent me) -> {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
if (me.isPrimaryButtonDown()) {
// this is done when the mouse is dragged and each rotation is
// performed in coordinates, that are rotated with the camera.
cameraXform.ry.setAngle(cameraXform.ry.getAngle() + mouseDeltaX * 0.2);
cameraXform.rx.setAngle(cameraXform.rx.getAngle() - mouseDeltaY * 0.2);
}
});
}
public static void main(String[] args) {
launch(args);
}
}
class XformWorld extends Group {
final Translate t = new Translate(0.0, 0.0, 0.0);
final Rotate rx = new Rotate(0, 0, 0, 0, Rotate.X_AXIS);
final Rotate ry = new Rotate(0, 0, 0, 0, Rotate.Y_AXIS);
final Rotate rz = new Rotate(0, 0, 0, 0, Rotate.Z_AXIS);
public XformWorld() {
super();
this.getTransforms().addAll(t, rx, ry, rz);
}
}
class XformCamera extends Group {
final Translate t = new Translate(0.0, 0.0, 0.0);
final Rotate rx = new Rotate(0, 0, 0, 0, Rotate.X_AXIS);
final Rotate ry = new Rotate(0, 0, 0, 0, Rotate.Y_AXIS);
final Rotate rz = new Rotate(0, 0, 0, 0, Rotate.Z_AXIS);
public XformCamera() {
super();
this.getTransforms().addAll(t, rx, ry, rz);
}
}
I found a solution that works for me, just in case someone else is interested too:
package trafotest;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.geometry.Point3D;
import javafx.scene.DepthTest;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
public class TrafoTest extends Application {
final Group root = new Group();
final XformWorld world = new XformWorld();
final PerspectiveCamera camera = new PerspectiveCamera(true);
final XformCamera cameraXform = new XformCamera();
private static final double CAMERA_INITIAL_DISTANCE = -1000;
private static final double CAMERA_NEAR_CLIP = 0.1;
private static final double CAMERA_FAR_CLIP = 10000.0;
double mousePosX, mousePosY, mouseOldX, mouseOldY, mouseDeltaX, mouseDeltaY;
double mouseFactorX, mouseFactorY;
#Override
public void start(Stage primaryStage) {
root.getChildren().add(world);
root.setDepthTest(DepthTest.ENABLE);
buildCamera();
buildBodySystem();
Scene scene = new Scene(root, 800, 600, true);
scene.setFill(Color.GREY);
handleMouse(scene);
primaryStage.setTitle("TrafoTest");
primaryStage.setScene(scene);
primaryStage.show();
scene.setCamera(camera);
mouseFactorX = 180.0 / scene.getWidth();
mouseFactorY = 180.0 / scene.getHeight();
}
private void buildCamera() {
root.getChildren().add(cameraXform);
cameraXform.getChildren().add(camera);
camera.setNearClip(CAMERA_NEAR_CLIP);
camera.setFarClip(CAMERA_FAR_CLIP);
camera.setTranslateZ(CAMERA_INITIAL_DISTANCE);
}
private void buildBodySystem() {
PhongMaterial whiteMaterial = new PhongMaterial();
whiteMaterial.setDiffuseColor(Color.WHITE);
whiteMaterial.setSpecularColor(Color.LIGHTBLUE);
Box box = new Box(400, 200, 100);
box.setMaterial(whiteMaterial);
PhongMaterial redMaterial = new PhongMaterial();
redMaterial.setDiffuseColor(Color.DARKRED);
redMaterial.setSpecularColor(Color.RED);
Sphere sphere = new Sphere(5);
sphere.setMaterial(redMaterial);
sphere.setTranslateX(200.0);
sphere.setTranslateY(-100.0);
sphere.setTranslateZ(-50.0);
world.getChildren().addAll(box);
world.getChildren().addAll(sphere);
}
private void handleMouse(Scene scene) {
scene.setOnMousePressed((MouseEvent me) -> {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
});
scene.setOnMouseDragged((MouseEvent me) -> {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
if (me.isPrimaryButtonDown()) {
cameraXform.ry(mouseDeltaX * 180.0 / scene.getWidth());
cameraXform.rx(-mouseDeltaY * 180.0 / scene.getHeight());
} else if (me.isSecondaryButtonDown()) {
camera.setTranslateZ(camera.getTranslateZ() + mouseDeltaY);
}
});
}
public static void main(String[] args) {
launch(args);
}
}
class XformWorld extends Group {
final Translate t = new Translate(0.0, 0.0, 0.0);
final Rotate rx = new Rotate(0, 0, 0, 0, Rotate.X_AXIS);
final Rotate ry = new Rotate(0, 0, 0, 0, Rotate.Y_AXIS);
final Rotate rz = new Rotate(0, 0, 0, 0, Rotate.Z_AXIS);
public XformWorld() {
super();
this.getTransforms().addAll(t, rx, ry, rz);
}
}
class XformCamera extends Group {
Point3D px = new Point3D(1.0, 0.0, 0.0);
Point3D py = new Point3D(0.0, 1.0, 0.0);
Rotate r;
Transform t = new Rotate();
public XformCamera() {
super();
}
public void rx(double angle) {
r = new Rotate(angle, px);
this.t = t.createConcatenation(r);
this.getTransforms().clear();
this.getTransforms().addAll(t);
}
public void ry(double angle) {
r = new Rotate(angle, py);
this.t = t.createConcatenation(r);
this.getTransforms().clear();
this.getTransforms().addAll(t);
}
}

Categories

Resources