Zoom selection rectangle in JavaFX Chart not showing, though Zoom working - javafx

I have written a sample program that implements Zooming function on a JavaFX Chart, I found the Zoom class from the GitHub project and am just reusing it. My challenge is that when I drag the mouse to select some region to Zoom, the selected area rectangle doesn't show in Windows 7, Linux, Mac OS X, but it works fine in Windows 10.
What am I missing, how can I make the selectedRectangle to show so the user can know what area they are zooming into?
Below are all the files that are needed to compile and run this program:
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package testlinechartgraphs;
import java.util.ArrayList;
import java.util.Random;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class TestLineChartGraphs extends Application {
final static ObservableList<XYChart.Series<Number, Number>> lineChartData = FXCollections.observableArrayList();
#Override
public void start(Stage stage) {
stage.setTitle("Line Chart Sample");
//defining the axes
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("Number of Month");
Random randomNumbers = new Random();
ArrayList<Integer> arrayList = new ArrayList<>();
//creating the chart
final LineChart<Number, Number> lineChart
= new LineChart<Number, Number>(xAxis, yAxis);
lineChart.setTitle("Stock Monitoring, 2010");
lineChart.setLegendSide(Side.RIGHT);
int randomCount = randomNumbers.nextInt(14)+1;
//System.out.println("randomCount = " + randomCount);
for (int i = 0; i < randomCount; i++) {
XYChart.Series series = new XYChart.Series();
series.setName("series_" + i);
for (int k = 0; k < 20; k++) {
int x = randomNumbers.nextInt(50);
series.getData().add(new XYChart.Data(k, x));
}
//seriesList.add(series);
lineChartData.add(series);
}
lineChart.setData(lineChartData);
final StackPane chartContainer = new StackPane();
Zoom zoom = new Zoom(lineChart, chartContainer);
chartContainer.getChildren()
.add(lineChart);
BorderPane borderPane = new BorderPane();
borderPane.setCenter(chartContainer);
//borderPane.setCenter(lineChart);
borderPane.setBottom(getLegend());
////
//Scene scene = new Scene(lineChart, 800, 600);
Scene scene = new Scene(borderPane, 800, 600);
//lineChart.getData().addAll(series, series1);
stage.setScene(scene);
scene.getStylesheets().addAll("file:///C:/Users/siphoh/Documents/NetBeansProjects/WiresharkSeqNum/src/fancychart.css");
//scene.getStylesheets().addAll(getClass().getResource("fancychart.css").toExternalForm());
stage.show();
}
public static Node getLegend() {
HBox hBox = new HBox();
for (final XYChart.Series<Number, Number> series : lineChartData) {
CheckBox checkBox = new CheckBox(series.getName());
checkBox.setSelected(true);
checkBox.setOnAction(event -> {
if (lineChartData.contains(series)) {
lineChartData.remove(series);
} else {
lineChartData.add(series);
}
});
hBox.getChildren().add(checkBox);
}
hBox.setAlignment(Pos.CENTER);
hBox.setSpacing(20);
hBox.setStyle("-fx-padding: 0 10 20 10");
return hBox;
}
public static void main(String[] args) {
launch(args);
}
}
//Zoom.java class:
package testlinechartgraphs;
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
/**
*
* #author *************
*/
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Label;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
/**
* This class adds a zoom functionality to a given XY chart. Zoom means that a
user can select a region in the chart that should be displayed at a larger
scale.
*
*/
public class Zoom {
private static final String INFO_LABEL_ID = "zoomInfoLabel";
private final Pane pane;
private final XYChart<Number, Number> chart;
private final NumberAxis xAxis;
private final NumberAxis yAxis;
private final SelectionRectangle selectionRectangle;
private Label infoLabel;
private Point2D selectionRectangleStart;
private Point2D selectionRectangleEnd;
/**
* Create a new instance of this class with the given chart and pane
* instances. The {#link Pane} instance is needed as a parent for the
* rectangle that represents the user selection.
*
* #param chart the xy chart to which the zoom support should be added
* #param pane the pane on which the selection rectangle will be drawn.
*/
public Zoom(XYChart<Number, Number> chart, Pane pane) {
this.pane = pane;
this.chart = chart;
this.xAxis = (NumberAxis) chart.getXAxis();
this.yAxis = (NumberAxis) chart.getYAxis();
selectionRectangle = new SelectionRectangle();
pane.getChildren().add(selectionRectangle);
addDragSelectionMechanism();
addInfoLabel();
}
/**
* The info label shows a short info text that tells the user how to unreset
* the zoom level.
*/
private void addInfoLabel() {
infoLabel = new Label("Click ESC to reset the zoom level.");
infoLabel.setId(INFO_LABEL_ID);
pane.getChildren().add(infoLabel);
StackPane.setAlignment(infoLabel, Pos.TOP_RIGHT);
infoLabel.setVisible(false);
}
/**
* Adds a mechanism to select an area in the chart that should be displayed
* at larged scale.
*/
private void addDragSelectionMechanism() {
pane.addEventHandler(MouseEvent.MOUSE_PRESSED, new MousePressedHandler());
pane.addEventHandler(MouseEvent.MOUSE_DRAGGED, new MouseDraggedHandler());
pane.addEventHandler(MouseEvent.MOUSE_RELEASED, new MouseReleasedHandler());
pane.addEventHandler(KeyEvent.KEY_RELEASED, new EscapeKeyHandler());
}
private Point2D computeRectanglePoint(double eventX, double eventY) {
double lowerBoundX = computeOffsetInChart(xAxis, false);
double upperBoundX = lowerBoundX + xAxis.getWidth();
double lowerBoundY = computeOffsetInChart(yAxis, true);
double upperBoundY = lowerBoundY + yAxis.getHeight();
// make sure the rectangle's end point is in the interval defined by the lower and upper bounds for each
// dimension
double x = Math.max(lowerBoundX, Math.min(eventX, upperBoundX));
double y = Math.max(lowerBoundY, Math.min(eventY, upperBoundY));
return new Point2D(x, y);
}
/**
* Computes the pixel offset of the given node inside the chart node.
*
* #param node the node for which to compute the pixel offset
* #param vertical flag that indicates whether the horizontal or the
* vertical dimension should be taken into account
* #return the offset inside the chart node
*/
private double computeOffsetInChart(Node node, boolean vertical) {
double offset = 0;
do {
if (vertical) {
offset += node.getLayoutY();
} else {
offset += node.getLayoutX();
}
node = node.getParent();
} while (node != chart);
return offset;
}
/**
*
*/
private final class MousePressedHandler implements EventHandler<MouseEvent> {
#Override
public void handle(final MouseEvent event) {
// do nothing for a right-click
if (event.isSecondaryButtonDown()) {
return;
}
// store position of initial click
selectionRectangleStart = computeRectanglePoint(event.getX(), event.getY());
event.consume();
}
}
/**
*
*/
private final class MouseDraggedHandler implements EventHandler<MouseEvent> {
#Override
public void handle(final MouseEvent event) {
// do nothing for a right-click
if (event.isSecondaryButtonDown()) {
return;
}
// store current cursor position
selectionRectangleEnd = computeRectanglePoint(event.getX(), event.getY());
double x = Math.min(selectionRectangleStart.getX(), selectionRectangleEnd.getX());
double y = Math.min(selectionRectangleStart.getY(), selectionRectangleEnd.getY());
double width = Math.abs(selectionRectangleStart.getX() - selectionRectangleEnd.getX());
double height = Math.abs(selectionRectangleStart.getY() - selectionRectangleEnd.getY());
drawSelectionRectangle(x, y, width, height);
event.consume();
}
/**
* Draws a selection box in the view.
*
* #param x the x position of the selection box
* #param y the y position of the selection box
* #param width the width of the selection box
* #param height the height of the selection box
*/
private void drawSelectionRectangle(final double x, final double y, final double width, final double height) {
selectionRectangle.setVisible(true);
selectionRectangle.setX(x);
selectionRectangle.setY(y);
selectionRectangle.setWidth(width);
selectionRectangle.setHeight(height);
//selectionRectangle.setFill(Color.LIGHTSEAGREEN.deriveColor(0, 1, 1, 0.5));
//System.out.println("Draw the rectangle ...");
}
}
/**
*
*/
private final class MouseReleasedHandler implements EventHandler<MouseEvent> {
/**
* Defines a minimum width for the selected area. If the selected
* rectangle is not wider than this value, no zooming will take place.
* This helps prevent accidental zooming.
*/
private static final double MIN_RECTANGE_WIDTH = 10;
/**
* Defines a minimum height for the selected area. If the selected
* rectangle is not wider than this value, no zooming will take place.
* This helps prevent accidental zooming.
*/
private static final double MIN_RECTANGLE_HEIGHT = 10;
#Override
public void handle(final MouseEvent event) {
hideSelectionRectangle();
if (selectionRectangleStart == null || selectionRectangleEnd == null) {
return;
}
if (isRectangleSizeTooSmall()) {
return;
}
setAxisBounds();
showInfo();
selectionRectangleStart = null;
selectionRectangleEnd = null;
// needed for the key event handler to receive events
pane.requestFocus();
event.consume();
}
private boolean isRectangleSizeTooSmall() {
double width = Math.abs(selectionRectangleEnd.getX() - selectionRectangleStart.getX());
double height = Math.abs(selectionRectangleEnd.getY() - selectionRectangleStart.getY());
return width < MIN_RECTANGE_WIDTH || height < MIN_RECTANGLE_HEIGHT;
}
/**
* Hides the selection rectangle.
*/
private void hideSelectionRectangle() {
selectionRectangle.setVisible(false);
}
private void setAxisBounds() {
disableAutoRanging();
// compute new bounds for the chart's x and y axes
double selectionMinX = Math.min(selectionRectangleStart.getX(), selectionRectangleEnd.getX());
double selectionMaxX = Math.max(selectionRectangleStart.getX(), selectionRectangleEnd.getX());
double selectionMinY = Math.min(selectionRectangleStart.getY(), selectionRectangleEnd.getY());
double selectionMaxY = Math.max(selectionRectangleStart.getY(), selectionRectangleEnd.getY());
setHorizontalBounds(selectionMinX, selectionMaxX);
setVerticalBounds(selectionMinY, selectionMaxY);
}
private void disableAutoRanging() {
xAxis.setAutoRanging(false);
yAxis.setAutoRanging(false);
}
private void showInfo() {
infoLabel.setVisible(true);
}
/**
* Sets new bounds for the chart's x axis.
*
* #param minPixelPosition the x position of the selection rectangle's
* left edge (in pixels)
* #param maxPixelPosition the x position of the selection rectangle's
* right edge (in pixels)
*/
private void setHorizontalBounds(double minPixelPosition, double maxPixelPosition) {
double currentLowerBound = xAxis.getLowerBound();
double currentUpperBound = xAxis.getUpperBound();
double offset = computeOffsetInChart(xAxis, false);
setLowerBoundX(minPixelPosition, currentLowerBound, currentUpperBound, offset);
setUpperBoundX(maxPixelPosition, currentLowerBound, currentUpperBound, offset);
}
/**
* Sets new bounds for the chart's y axis.
*
* #param minPixelPosition the y position of the selection rectangle's
* upper edge (in pixels)
* #param maxPixelPosition the y position of the selection rectangle's
* lower edge (in pixels)
*/
private void setVerticalBounds(double minPixelPosition, double maxPixelPosition) {
double currentLowerBound = yAxis.getLowerBound();
double currentUpperBound = yAxis.getUpperBound();
double offset = computeOffsetInChart(yAxis, true);
setLowerBoundY(maxPixelPosition, currentLowerBound, currentUpperBound, offset);
setUpperBoundY(minPixelPosition, currentLowerBound, currentUpperBound, offset);
}
private void setLowerBoundX(double pixelPosition, double currentLowerBound, double currentUpperBound,
double offset) {
double newLowerBound = computeBound(pixelPosition, offset, xAxis.getWidth(), currentLowerBound,
currentUpperBound, false);
xAxis.setLowerBound(newLowerBound);
}
private void setUpperBoundX(double pixelPosition, double currentLowerBound, double currentUpperBound,
double offset) {
double newUpperBound = computeBound(pixelPosition, offset, xAxis.getWidth(), currentLowerBound,
currentUpperBound, false);
xAxis.setUpperBound(newUpperBound);
}
private void setLowerBoundY(double pixelPosition, double currentLowerBound, double currentUpperBound,
double offset) {
double newLowerBound = computeBound(pixelPosition, offset, yAxis.getHeight(), currentLowerBound,
currentUpperBound, true);
yAxis.setLowerBound(newLowerBound);
}
private void setUpperBoundY(double pixelPosition, double currentLowerBound, double currentUpperBound,
double offset) {
double newUpperBound = computeBound(pixelPosition, offset, yAxis.getHeight(), currentLowerBound,
currentUpperBound, true);
yAxis.setUpperBound(newUpperBound);
}
private double computeBound(double pixelPosition, double pixelOffset, double pixelLength, double lowerBound,
double upperBound, boolean axisInverted) {
double pixelPositionWithoutOffset = pixelPosition - pixelOffset;
double relativePosition = pixelPositionWithoutOffset / pixelLength;
double axisLength = upperBound - lowerBound;
// The screen's y axis grows from top to bottom, whereas the chart's y axis goes from bottom to top.
// That's
// why we need to have this distinction here.
double offset = 0;
int sign = 0;
if (axisInverted) {
offset = upperBound;
sign = -1;
} else {
offset = lowerBound;
sign = 1;
}
double newBound = offset + sign * relativePosition * axisLength;
return newBound;
}
}
/**
*
*/
private final class EscapeKeyHandler implements EventHandler<KeyEvent> {
#Override
public void handle(KeyEvent event) {
// the ESCAPE key lets the user reset the zoom level
if (KeyCode.ESCAPE.equals(event.getCode())) {
resetAxisBounds();
hideInfo();
}
}
private void resetAxisBounds() {
xAxis.setAutoRanging(true);
yAxis.setAutoRanging(true);
}
private void hideInfo() {
infoLabel.setVisible(false);
}
}
}
//SelectionRectangle.java class
package testlinechartgraphs;
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
/**
*
* #author **************
*/
import javafx.scene.shape.Rectangle;
/**
* Represents an area on the screen that was selected by a mouse drag operation.
*
*/
public class SelectionRectangle extends Rectangle {
private static final String STYLE_CLASS_SELECTION_BOX = "chart-selection-rectangle";
public SelectionRectangle() {
getStyleClass().addAll(STYLE_CLASS_SELECTION_BOX);
setVisible(false);
setManaged(false);
setMouseTransparent(true);
}
}
// fancychart.css file
.chart-line-symbol {
-fx-scale-x: 0.5;
-fx-scale-y: 0.5;
}
.chart-popup-label {
-fx-padding: 1 3 1 3;
-fx-border-radius: 1;
-fx-border-width: 1;
-fx-opacity: 0.7;
-fx-effect: dropshadow( two-pass-box , rgba(0,0,0,0.3) , 8, 0.0 , 0 , 3 );
}
.chart-legend-item {
-fx-padding : 1 23 1 23;
}
.chart-legend-item-symbol {
-fx-scale-x: 0.8;
-fx-scale-y: 0.8;
}
.chart-selection-rectangle {
-fx-stroke: rgba(135, 206, 250, 0.8);
-fx-stroke-type: inside;
-fx-fill: rgba(135, 206, 250, 0.2);
}
#zoomInfoLabel {
-fx-background-color: rgba(135, 206, 250, 0.8);
-fx-font-size: 14;
-fx-padding: 3;
-fx-background-radius: 2;
}
Any help will be greatly appreciated to resolve this issue.
thanks,

Your order is wrong. You have
final StackPane chartContainer = new StackPane();
Zoom zoom = new Zoom(lineChart, chartContainer);
chartContainer.getChildren().add(lineChart);
Which means you first create the container, then add the zoom rectangle, then add the chart. So the zoom rectangle is on the back of the chart.
You need to have it this way:
final StackPane chartContainer = new StackPane();
chartContainer.getChildren().add(lineChart);
Zoom zoom = new Zoom(lineChart, chartContainer);

Related

JavaFX 3D: Rotating PerspectiveCamera using keyboard ends up changing camera's view

I want to be able to rotate a perspective camera using the WASD keys. Pressing the W and S keys should rotate the camera along the X axis, and pressing the A and D keys should rotate it along the Y. In an attempt to do this I created a class called RotateCamera that extends the PerspectiveCamera class and includes methods I made to rotate the camera. This way mostly works, but I find if I first move the camera forward using the up arrow key, and then spam the W key so that the camera rotates 360 degrees, when the sphere comes back in view it is very far away, and moving the camera using the arrow keys seems to move it in random directions. I was wondering why this is/how to fix it? A copy of my classes is below.
package ui;
import javafx.scene.PerspectiveCamera;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Transform;
/**
* A PerspectiveCamera that includes methods which allow it to be rotated
*/
public class RotateCamera extends PerspectiveCamera {
Transform currTransform;
/**
* Constructor
* Creates a new camera with default Rotate transform
*/
public RotateCamera() {
currTransform = new Rotate();
}
/**
* Rotates the camera along the x-axis according to the given angle
* #param angle the amount of degrees the camera is rotated
*/
public void rotateInX(int angle) {
Rotate newRotation = new Rotate(angle, Rotate.X_AXIS);
rotateCam(newRotation);
}
/**
* Rotates the camera along the y-axis according to the given angle
* #param angle the amount of degrees the camera is rotated
*/
public void rotateInY(int angle) {
Rotate newRotation = new Rotate(angle, Rotate.Y_AXIS);
rotateCam(newRotation);
}
/**
* Applies both the currTransform and given rotation to the camera
*/
private void rotateCam(Rotate rotation) {
currTransform = currTransform.createConcatenation(rotation);
getTransforms().clear();
getTransforms().addAll(currTransform);
}
}
>
package ui;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Sphere;
import javafx.stage.Stage;
import javafx.scene.Group;
import model.Molecule;
import java.util.Observable;
import java.util.Observer;
/**
* Represents the window in which the molecule drawing editor appears.
* Run the main method to start the program!
*/
public class MarsBonds extends Application {
private static final int CAM_NEAR_CLIP = 0;
private static final int CAM_FAR_CLIP = 1000;
private static final int CAM_ORG_DISTANCE = -10;
private static final int WIDTH = 1000;
private static final int HEIGHT = 800;
private static final Color SCENE_COLOR = Color.BLACK;
private static final int CAM_SPEED = 30;
private static Stage primaryStage;
private static RotateCamera camera;
private static Scene scene;
#Override
public void start(Stage primaryStage) throws Exception {
setScene();
setCamera();
setPrimaryState(primaryStage);
}
/**
* Creates scene with molecule in center of screen and black background
*/
public static void setScene() {
Sphere molecule = new Sphere(30);
molecule.translateXProperty().set(WIDTH/2);
molecule.translateYProperty().set(HEIGHT/2);
Group root = new Group();
root.getChildren().add(molecule);
scene = new Scene(root, WIDTH, HEIGHT);
scene.setFill(SCENE_COLOR);
}
/**
* Initializes camera and adds to scene
*/
private static void setCamera() {
camera = new RotateCamera();
camera.setNearClip(CAM_NEAR_CLIP);
camera.setFarClip(CAM_FAR_CLIP);
camera.translateZProperty().set(CAM_ORG_DISTANCE);
scene.setCamera(camera);
}
/**
* Sets up the primary stage by setting its scene, title, and adding key control
* #param stage the primary stage
*/
private static void setPrimaryState(Stage stage) {
primaryStage = stage;
addEventHandlers();
primaryStage.setTitle("Mar's Bonds");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* Adds KeyEvent handler to primary stage
* The KeyEvent handler uses input from the WASD keys to rotate
* the camera and the arrow keys to move the camera
*/
private static void addEventHandlers() {
primaryStage.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
switch(e.getCode()) {
case W:
camera.rotateInX(-1);
break;
case S:
camera.rotateInX(1);
break;
case A:
camera.rotateInY(1);
break;
case D:
camera.rotateInY(-1);
break;
case UP:
camera.setTranslateZ(camera.getTranslateZ() + CAM_SPEED);
break;
case DOWN:
camera.setTranslateZ(camera.getTranslateZ() - CAM_SPEED);
break;
case LEFT:
camera.setTranslateX(camera.getTranslateX() - CAM_SPEED/3);
break;
case RIGHT:
camera.setTranslateX(camera.getTranslateX() + CAM_SPEED/3);
break;
}
});
}
public static void main( String[] args ) {
launch(args);
}
}

High refreshing rate in JavaFX

I'm trying to write a program with an equalizer, a frequency analyzer and a sound level meter. The model part seems to work very well but I'm experimenting some bugs with the IHM.
My last bug is with the level meter. After a while (from few milliseconds to few seconds), it freezes and don't update anymore. So, here is a (simplified) version of it. I added the runnable part to test and reproduce the bug. Of course, this bug appears sooner when I add other graphical components which also need to refresh very frequently. For example, the frequency analyze is represented by a line-chart with something like 1000 points.
public class LevelMeter2 extends Parent implements Runnable {
private IntegerProperty levelMeterHeight = new SimpleIntegerProperty();
private Rectangle led;
private IntegerProperty height = new SimpleIntegerProperty();
private IntegerProperty width = new SimpleIntegerProperty();
private DoubleProperty linearValue = new SimpleDoubleProperty();
private Color backgroundColor=Color.BLACK;
private double minLinearValue, maxLinearValue;
public LevelMeter2 (int height2, int width2) {
this.height.set(height2);
this.levelMeterHeight.bind(height.multiply(0.9));
this.width.set(width2);
linearValue.set(1.0);
minLinearValue = Math.pow(10, -60.0/100);
maxLinearValue = Math.pow(10, 3.0/100)-minLinearValue;
Rectangle levelMeterShape = new Rectangle();
levelMeterShape.widthProperty().bind(width);
levelMeterShape.heightProperty().bind(height);
levelMeterShape.setStroke(backgroundColor);
this.getChildren().add(levelMeterShape);
led = new Rectangle();
led.widthProperty().bind(width.multiply(0.8));
led.translateXProperty().bind(width.multiply(0.1));
led.heightProperty().bind(levelMeterHeight.multiply(linearValue));
led.setFill(Color.AQUA);
Rotate rotate = new Rotate();
rotate.pivotXProperty().bind(width.multiply(0.8).divide(2));
rotate.pivotYProperty().bind(height.divide(2));
rotate.setAngle(180);
led.getTransforms().add(rotate);
this.getChildren().add(led);
}
public double convertdBToLinearValue (double dB) {
return ((double)Math.round(100 * ((Math.pow(10, dB/100)-minLinearValue)/maxLinearValue)) ) /100 ;
//return (Math.pow(10, dB/100)-minLinearValue)/maxLinearValue;
}
public double convertLinearValueTodB (double linearValue) {
return 100*Math.log10(linearValue*maxLinearValue+minLinearValue);
}
public void setValue (double dB) {
if (dB>3) {
dB=3;
}
linearValue.setValue(convertdBToLinearValue(dB));
}
#Override
public void run() {
int i = 0;
double value=-20;
while (i<1000) {
setValue(value);
value = (Math.random()-0.5)*10+value;
if (value>3) {
value=3;
}
if (value<-60) {
value=-60;
}
i++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("END OF WHILE");
}
}
And a "Main" to test it :
public class MainGraph extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
HBox pane = new HBox();
LevelMeter2 levelMeter = new LevelMeter2(300,30);
Thread t = new Thread(levelMeter);
pane.getChildren().add(levelMeter);
t.start();
Scene scene = new Scene(pane, 300, 300);
primaryStage.setScene(scene);
primaryStage.setTitle("Test IHM");
primaryStage.setOnCloseRequest( event -> {
System.out.println("FIN");
System.exit(0);
});
primaryStage.show();
}
}
What's wrong with my code ? How can I write a more robust code that will allow me high refresh rates of my IHM ? Or how can I prevent from freezing ?
Thank you for you help.
I would suggest you move away from Threads and use something from JavaFX Animation package. In this example Timeline is used. This code is set to run at a rate of about 60 fps. You can adjust that using Duration.millis().
Main
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
/**
*
* #author blj0011
*/
public class JavaFXApplication342 extends Application
{
#Override
public void start(Stage primaryStage)
{
LevelMeter2 levelMeter = new LevelMeter2(300, 30);
Button button = new Button("Start");
button.setOnAction((event) -> {
switch (button.getText()) {
case "Start":
levelMeter.startAnimation();
button.setText("Stop");
break;
case "Stop":
levelMeter.stopAnimation();
button.setText("Start");
break;
}
});
HBox pane = new HBox(levelMeter, button);
Scene scene = new Scene(pane, 300, 300);
primaryStage.setScene(scene);
primaryStage.setTitle("Test IHM");
primaryStage.setOnCloseRequest(event -> {
System.out.println("FIN");
System.exit(0);
});
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args)
{
launch(args);
}
}
LevelMeter2
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Parent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Rotate;
import javafx.util.Duration;
public final class LevelMeter2 extends Parent
{
private final IntegerProperty levelMeterHeight = new SimpleIntegerProperty();
Timeline timeline;
double value = -20;
private final Rectangle led;
private final IntegerProperty height = new SimpleIntegerProperty();
private final IntegerProperty width = new SimpleIntegerProperty();
private final DoubleProperty linearValue = new SimpleDoubleProperty();
private final Color backgroundColor = Color.BLACK;
private final double minLinearValue;
private final double maxLinearValue;
public LevelMeter2(int height2, int width2)
{
this.height.set(height2);
this.levelMeterHeight.bind(height.multiply(0.9));
this.width.set(width2);
linearValue.set(1.0);
minLinearValue = Math.pow(10, -60.0 / 100);
maxLinearValue = Math.pow(10, 3.0 / 100) - minLinearValue;
Rectangle levelMeterShape = new Rectangle();
levelMeterShape.widthProperty().bind(width);
levelMeterShape.heightProperty().bind(height);
levelMeterShape.setStroke(backgroundColor);
this.getChildren().add(levelMeterShape);
led = new Rectangle();
led.widthProperty().bind(width.multiply(0.8));
led.translateXProperty().bind(width.multiply(0.1));
led.heightProperty().bind(levelMeterHeight.multiply(linearValue));
led.setFill(Color.AQUA);
Rotate rotate = new Rotate();
rotate.pivotXProperty().bind(width.multiply(0.8).divide(2));
rotate.pivotYProperty().bind(height.divide(2));
rotate.setAngle(180);
led.getTransforms().add(rotate);
getChildren().add(led);
timeline = new Timeline(new KeyFrame(Duration.millis(16), (event) -> {
setValue(value);
value = (Math.random() - 0.5) * 10 + value;
if (value > 3) {
value = 3;
}
if (value < -60) {
value = -60;
}
}));
timeline.setCycleCount(Timeline.INDEFINITE);
}
public double convertdBToLinearValue(double dB)
{
return ((double) Math.round(100 * ((Math.pow(10, dB / 100) - minLinearValue) / maxLinearValue))) / 100;
}
public double convertLinearValueTodB(double linearValue)
{
return 100 * Math.log10(linearValue * maxLinearValue + minLinearValue);
}
public void setValue(double dB)
{
if (dB > 3) {
dB = 3;
}
linearValue.setValue(convertdBToLinearValue(dB));
}
public void startAnimation()
{
timeline.play();
}
public void stopAnimation()
{
timeline.stop();
}
}
Multiple LevelMeters Example:
Main
import java.util.ArrayList;
import java.util.List;
import javafx.animation.ParallelTransition;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
*
* #author blj0011
*/
public class JavaFXApplication342 extends Application
{
#Override
public void start(Stage primaryStage)
{
List<LevelMeter2> levelMeter2s = new ArrayList();
List<Timeline> metersTimelines = new ArrayList();
for (int i = 0; i < 9; i++) {
LevelMeter2 levelMeter2 = new LevelMeter2(300, 30);
levelMeter2s.add(levelMeter2);
metersTimelines.add(levelMeter2.getTimeline());
}
ParallelTransition parallelTransition = new ParallelTransition();
parallelTransition.getChildren().addAll(metersTimelines);
Button button = new Button("Start");
button.setOnAction((event) -> {
switch (button.getText()) {
case "Start":
parallelTransition.play();
button.setText("Stop");
break;
case "Stop":
parallelTransition.stop();
button.setText("Start");
break;
}
});
HBox hBox = new HBox();
hBox.getChildren().addAll(levelMeter2s);
VBox vBox = new VBox(hBox, new StackPane(button));
Scene scene = new Scene(vBox, 300, 350);
primaryStage.setScene(scene);
primaryStage.setTitle("Test IHM");
primaryStage.setOnCloseRequest(event -> {
System.out.println("FIN");
System.exit(0);
});
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args)
{
launch(args);
}
}
LevelMeter2
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Parent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Rotate;
import javafx.util.Duration;
public final class LevelMeter2 extends Parent
{
private final IntegerProperty levelMeterHeight = new SimpleIntegerProperty();
Timeline timeline;
double value = -20;
private final Rectangle led;
private final IntegerProperty height = new SimpleIntegerProperty();
private final IntegerProperty width = new SimpleIntegerProperty();
private final DoubleProperty linearValue = new SimpleDoubleProperty();
private final Color backgroundColor = Color.BLACK;
private final double minLinearValue;
private final double maxLinearValue;
public LevelMeter2(int height2, int width2)
{
this.height.set(height2);
this.levelMeterHeight.bind(height.multiply(0.9));
this.width.set(width2);
linearValue.set(1.0);
minLinearValue = Math.pow(10, -60.0 / 100);
maxLinearValue = Math.pow(10, 3.0 / 100) - minLinearValue;
Rectangle levelMeterShape = new Rectangle();
levelMeterShape.widthProperty().bind(width);
levelMeterShape.heightProperty().bind(height);
levelMeterShape.setStroke(backgroundColor);
this.getChildren().add(levelMeterShape);
led = new Rectangle();
led.widthProperty().bind(width.multiply(0.8));
led.translateXProperty().bind(width.multiply(0.1));
led.heightProperty().bind(levelMeterHeight.multiply(linearValue));
led.setFill(Color.AQUA);
Rotate rotate = new Rotate();
rotate.pivotXProperty().bind(width.multiply(0.8).divide(2));
rotate.pivotYProperty().bind(height.divide(2));
rotate.setAngle(180);
led.getTransforms().add(rotate);
getChildren().add(led);
timeline = new Timeline(new KeyFrame(Duration.millis(25), (event) -> {
setValue(value);
value = (Math.random() - 0.5) * 10 + value;
if (value > 3) {
value = 3;
}
if (value < -60) {
value = -60;
}
}));
timeline.setCycleCount(Timeline.INDEFINITE);
}
public double convertdBToLinearValue(double dB)
{
return ((double) Math.round(100 * ((Math.pow(10, dB / 100) - minLinearValue) / maxLinearValue))) / 100;
}
public double convertLinearValueTodB(double linearValue)
{
return 100 * Math.log10(linearValue * maxLinearValue + minLinearValue);
}
public void setValue(double dB)
{
if (dB > 3) {
dB = 3;
}
linearValue.setValue(convertdBToLinearValue(dB));
}
public void startAnimation()
{
timeline.play();
}
public void stopAnimation()
{
timeline.stop();
}
public Timeline getTimeline()
{
return timeline;
}
}
Your implementation of run() appears to be updating the scene graph from a background thread. As discussed in Concurrency in JavaFX:
The JavaFX scene graph…is not thread-safe and can only be accessed and modified from the UI thread also known as the JavaFX Application thread. Implementing long-running tasks on the JavaFX Application thread inevitably makes an application UI unresponsive."
Instead, use a Task, illustrated here and here. Your implementation of call() can collect data asynchronously and notify the GUI of the current state via updateValue(). Your valueProperty() listener can then invoke setValue() safely. Because "Updates are coalesced to prevent saturation of the FX event queue," your application will perform satisfactorily even on older hardware.
Alternatively, if your audio source is one of the supported Media types, AudioBarChartApp, also seen here, updates the data model of a BarChart in an AudioSpectrumListener registered with the corresponding MediaPlayer. The image below displays pink noise.
private XYChart.Data<String, Number>[] series1Data;
…
audioSpectrumListener = (double timestamp, double duration,
float[] magnitudes, float[] phases) -> {
for (int i = 0; i < series1Data.length; i++) {
series1Data[i].setYValue(magnitudes[i] + 60);
}
};

Zooming and panning image in scrollpane

I have a problem with zooming and panning image in ScrollPane. So far I have code like this:
Image image = imageView.getImage();
scrollPane.setPrefViewportWidth(0d);
scrollPane.setPrefViewportHeight(0d);
imageView.setFitWidth(0d);
imageView.setFitHeight(0d);
Bounds viewportBounds = scrollPane.getViewportBounds();
boolean vertical = image.getWidth() > image.getHeight();
if (imageView.getRotate() == 90 || imageView.getRotate() == 270d) {
vertical = !vertical;
}
imageView.setPreserveRatio(true);
double propX = viewportBounds.getWidth() / image.getWidth();
double propY = viewportBounds.getHeight() / image.getHeight();
boolean xLead = !(propX > propY);
imageView.setScaleX((xLead) ? propX : propY);
imageView.setScaleY((xLead) ? propX : propY);
scrollPane.setContent(imageView);
scrollPane.setPannable(true);
scrollPane.setHvalue(scrollPane.getHmin() + (scrollPane.getHmax() - scrollPane.getHmin()) / 2); // center the scroll contents.
scrollPane.setVvalue(scrollPane.getVmin() + (scrollPane.getVmax() - scrollPane.getVmin()) / 2);
zoom(imageView);
private void zoom(ImageView imagePannable) {
imagePannable.setOnScroll(
new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double zoomFactor = 1.20;
double deltaY = event.getDeltaY();
if (deltaY < 0) {
zoomFactor = 0.80;
}
imagePannable.setScaleX(imagePannable.getScaleX() * zoomFactor);
imagePannable.setScaleY(imagePannable.getScaleY() * zoomFactor);
event.consume();
}
});
}
What I want to do is align image relative to mouse pointer not to center of image everytime.
I also have a problem with large images (like maps which are 8*A4 size for example). When I zooming this maps pannable function stop working. What is wrong with this code? Thanks for helps!
Several people (including me) have had this same question. I got my answer here.
In the interest of clarity, here is a working example of a panning & zooming pane using a rectangle as the zoomed node. I have implemented this in a slightly more complex way with an ImageView.
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 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.NAVY);
rect.setStrokeType(StrokeType.INSIDE);
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();
}
public void fitWidth () {
double scale = getParent().getLayoutBounds().getMaxX()/getLayoutBounds().getMaxX();
double oldScale = getScale();
double f = scale - oldScale;
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();
}
}
}
};
}
}

Make 3D object illuminate differently over time in JavaFX

Need to make a sphere in JavaFX "blink" over time, like fade in and fade out. Is it possible? Or at least change shades of color. I managed to make it blink by changing SelfIlluminationMap property of the PhongMaterial I am using, with this code
import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.image.Image;
import javafx.scene.paint.*;
import javafx.scene.shape.*;
import javafx.scene.transform.*;
import javafx.stage.Stage;
import javafx.util.Duration;
public class JavaFXApplication15 extends Application {
Image im = new Image("bump.jpg");
PhongMaterial ph = new PhongMaterial(Color.GREEN);
int nums = 0;
UpdateTimer timer;
private class UpdateTimer extends AnimationTimer {
int counter = 0;
#Override
public void handle(long now) {
if (counter == 20) {
update();
counter = 0;
}
counter++;
}
}
private Parent createContent() throws Exception {
timer = new UpdateTimer();
ph = new PhongMaterial(Color.YELLOW, null, null, null, im);
Box box = new Box(5, 5, 5);
box.setMaterial(ph);
// Create and position camera
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.getTransforms().addAll(
new Rotate(-20, Rotate.X_AXIS),
new Translate(0, 0, -50)
);
// Build the Scene Graph
Group root = new Group();
root.getChildren().add(camera);
root.getChildren().add(box);
// Use a SubScene
SubScene subScene = new SubScene(
root,
300, 300,
true,
SceneAntialiasing.BALANCED
);
subScene.setFill(Color.ALICEBLUE);
subScene.setCamera(camera);
Group group = new Group();
group.getChildren().add(subScene);
return group;
}
void update() {
if (ph.getSelfIlluminationMap() != null) {
ph.setSelfIlluminationMap(null);
} else {
ph.setSelfIlluminationMap(im);
}
}
#Override
public void start(Stage stage) throws Exception {
stage.setResizable(false);
Scene scene = new Scene(createContent());
stage.setScene(scene);
stage.show();
timer.start();
}
public static void main(String[] args) {
launch(args);
}
}
but is it possible to make it fade in and out? With some kind of transition possibly?
Just to see the effect, I tried animating the specularColor of a sphere's PhongMaterial. Starting from this example, I followed the approach shown here to get a color lookup table of equally spaced brightness values of a given hue.
private final Queue<Color> clut = new LinkedList<>();
The implementation of handle() simply cycles through the table.
#Override
public void handle(long now) {
redMaterial.setSpecularColor(clut.peek());
clut.add(clut.remove());
}
If the result is appealing, a more flexible approach might accrue from using a concrete subclass of Transition.
private final Animation animation = new Transition() {
{
setCycleDuration(Duration.millis(1000));
setAutoReverse(true);
setCycleCount(INDEFINITE);
}
#Override
protected void interpolate(double d) {
redMaterial.setSpecularColor(Color.hsb(LITE.getHue(), 1, d));
redMaterial.setDiffuseColor(Color.hsb(DARK.getHue(), 1, d / 2));
}
};
import java.util.LinkedList;
import java.util.Queue;
import javafx.animation.AnimationTimer;
import javafx.animation.Animation;
import javafx.animation.Transition;
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;
import javafx.util.Duration;
/**
* #see https://stackoverflow.com/a/44447913/230513
* #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 static final Color LITE = Color.RED;
private static final Color DARK = Color.RED.darker().darker();
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 final PhongMaterial redMaterial = new PhongMaterial();
private final UpdateTimer timer = new UpdateTimer();
private class UpdateTimer extends AnimationTimer {
private static final double N = 32d;
private final Queue<Color> clut = new LinkedList<>();
public UpdateTimer() {
for (int i = 0; i < N; i++) {
clut.add(Color.hsb(LITE.getHue(), 1, 1 - (i / N)));
}
for (int i = 0; i < N; i++) {
clut.add(Color.hsb(LITE.getHue(), 1, i / N));
}
}
#Override
public void handle(long now) {
redMaterial.setSpecularColor(clut.peek());
clut.add(clut.remove());
}
}
private final Animation animation = new Transition() {
{
setCycleDuration(Duration.millis(1000));
setAutoReverse(true);
setCycleCount(INDEFINITE);
}
#Override
protected void interpolate(double d) {
redMaterial.setSpecularColor(Color.hsb(LITE.getHue(), 1, d));
redMaterial.setDiffuseColor(Color.hsb(DARK.getHue(), 1, d / 2));
}
};
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);
redMaterial.setDiffuseColor(DARK);
redMaterial.setSpecularColor(LITE);
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);
}
});
scene.setOnScroll((final ScrollEvent e) -> {
camera.setTranslateZ(camera.getTranslateZ() + e.getDeltaY());
});
primaryStage.show();
//content.timer.start();
content.animation.play();
}
public static void main(String[] args) {
launch(args);
}
}

JavaFX: Rotate an object without turning the axis

In order to work on a project Rybi's cube game, I have to turn the cubes around particular axes, so the problem is that if I turn an object around an axis, eg Y-axis with a 90 degree, so if I turn it again around x axis,the rotation will be on the direction of the Z axis because the X axis takes dircetion of Z.
Below, there is a piece of code illustrates the same situation that I just introduce you.
Is there a way to do things the way I desire
public class RybiCube extends Application {
final Group root = new Group();
final PerspectiveCamera camera = new PerspectiveCamera(true);
final XformCamera cameraXform = new XformCamera();
public static final double CAMERA_INITIAL_DISTANCE = -1000;
public static final double CAMERA_NEAR_CLIP = 0.1;
public static final double CAMERA_FAR_CLIP = 10000.0;
public void init(Stage primaryStage) {
Box box = new Box();
box.setHeight(70);
box.setWidth(200);
box.setDepth(70);
//box.setRotationAxis(Rotate.Y_AXIS);
//box.setRotate(80);
box.getTransforms().add(new Rotate(90,Rotate.Y_AXIS));
box.getTransforms().add(new Rotate(45,Rotate.X_AXIS));
PhongMaterial material = new PhongMaterial();
material.setDiffuseColor(Color.ORANGE);
material.setSpecularColor(Color.BLACK);
box.setMaterial(material);
root.getChildren().add(box);
buildCamera();
Scene scene = new Scene(root, 600, 600, true);
primaryStage.setScene(scene);
scene.setCamera(camera);
///
primaryStage.setResizable(false);
scene.setFill(Color.rgb(0, 0,0,0.5));
primaryStage.setScene(scene);
}
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);
}
public void start(Stage primaryStage)
{
init(primaryStage);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
class XformCamera extends Group {
final Translate tr = new Translate(0.0, 0.0, 0.0);
final Rotate rx = new Rotate(00, Rotate.X_AXIS);
final Rotate ry = new Rotate(10, Rotate.Y_AXIS);
final Rotate rz = new Rotate(0, Rotate.Z_AXIS);
public XformCamera() {
super();
this.getTransforms().addAll(tr, rx, ry, rz);
}
}
You could simply apply the inverse transformations to the axis (using inverseDeltaTransform):
public static void addRotate(Node node, Point3D rotationAxis, double angle) {
ObservableList<Transform> transforms = node.getTransforms();
try {
for (Transform t : transforms) {
rotationAxis = t.inverseDeltaTransform(rotationAxis);
}
} catch (NonInvertibleTransformException ex) {
throw new IllegalStateException(ex);
}
transforms.add(new Rotate(angle, rotationAxis));
}
box.getTransforms().add(new Rotate(90,Rotate.Y_AXIS));
addRotate(box, Rotate.X_AXIS, 45);
the following code may be useful for your application, it is based a 3D orientation.
import javafx.scene.Group;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class XformOrientation extends Group {
private static final int SIZE = 5;
private Affine affine;
private final List<Double> averageRoll = new ArrayList();
private final List<Double> averagePitch = new ArrayList();
private final List<Double> averageYaw = new ArrayList();
public XformOrientation() {
super();
affine = new Affine();
this.getTransforms().add(affine);
}
/**
* angles in degrees
* #param roll
* #param pitch
* #param yaw
*/
public void processEvent(double roll, double pitch, double yaw) {
double avYaw = average(averageYaw, Math.toRadians(yaw));
double avPitch = average(averagePitch, Math.toRadians(pitch));
double avRoll = average(averageRoll, Math.toRadians(roll));
matrixRotateNode(avRoll, avPitch, avYaw);
}
private void matrixRotateNode(double roll, double pitch, double yaw) {
double mxx = Math.cos(pitch) * Math.cos(yaw);
double mxy = Math.cos(roll) * Math.sin(pitch) +
Math.cos(pitch) * Math.sin(roll) * Math.sin(yaw);
double mxz = Math.sin(pitch) * Math.sin(roll) -
Math.cos(pitch) * Math.cos(roll) * Math.sin(yaw);
double myx = -Math.cos(yaw) * Math.sin(pitch);
double myy = Math.cos(pitch) * Math.cos(roll) -
Math.sin(pitch) * Math.sin(roll) * Math.sin(yaw);
double myz = Math.cos(pitch) * Math.sin(roll) +
Math.cos(roll) * Math.sin(pitch) * Math.sin(yaw);
double mzx = Math.sin(yaw);
double mzy = -Math.cos(yaw) * Math.sin(roll);
double mzz = Math.cos(roll) * Math.cos(yaw);
affine.setToTransform(mxx, mxy, mxz, 0,
myx, myy, myz, 0,
mzx, mzy, mzz, 0);
}
private double average(List<Double> list, double value) {
while (list.size() > SIZE) {
list.remove(0);
}
list.add(value);
return list.stream()
.collect(Collectors.averagingDouble(d -> d));
}
}
as an application, you can use
private final XformOrientation world = new XformOrientation();
PhongMaterial whiteMaterial = new PhongMaterial();
whiteMaterial.setDiffuseColor(Color.WHITE);
whiteMaterial.setSpecularColor(Color.LIGHTBLUE);
Box box = new Box(400, 200, 100);
box.setMaterial(whiteMaterial);
world.getChildren().addAll(box);
Runnable runnable = new Runnable() {
#Override
public void run() {
Platform.runLater(() -> {
world.processEvent(getRoll(), getPitch(), getYaw());
});
}
};
getAhrsInfo().rollProperty().addListener((observable, oldValue, newValue) -> runnable.run());
getAhrsInfo().pitchProperty().addListener((observable, oldValue, newValue) -> runnable.run());
getAhrsInfo().yawProperty().addListener((observable, oldValue, newValue) -> runnable.run());

Resources