I'm trying to write a painting application in JavaFX. I want a brush resembling a real paintbrush, but I'm not sure how to start the algorithm. The code below shows my current paintbrush stroke, although it's a useful stroke, it's not really a paintbrush:
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;
import javafx.stage.Stage;
import static javafx.scene.input.MouseEvent.*;
public class BrushTester extends Application {
private static final Color color = Color.CHOCOLATE;
private static final double START_OPACITY = 0.3;
private static final double OPACITY_MODIFIER = 0.002;
private double currentOpacity = START_OPACITY;
private double strokeWidth = 15;
public static void main(String[] args) {
Application.launch(BrushTester.class);
}
#Override
public void start(Stage primaryStage) throws Exception {
Canvas canvas = new Canvas(600d, 600d);
GraphicsContext gc = canvas.getGraphicsContext2D();
canvas.addEventHandler(MOUSE_DRAGGED, e -> BrushTester.this.handleMouseDragged(gc, e));
canvas.addEventHandler(MOUSE_PRESSED, e -> handleMousePressed(gc, e));
canvas.addEventHandler(MOUSE_RELEASED, e -> handleMouseReleased(gc, e));
Group root = new Group();
root.getChildren().add(canvas);
primaryStage.setScene(new Scene(root, Color.DARKGRAY));
primaryStage.show();
}
private void configureGraphicsContext(GraphicsContext gc) {
gc.setStroke(new Color(color.getRed(), color.getGreen(), color.getBlue(), currentOpacity));
gc.setLineCap(StrokeLineCap.ROUND);
gc.setLineJoin(StrokeLineJoin.ROUND);
gc.setLineWidth(strokeWidth);
}
public void handleMousePressed(GraphicsContext gc, MouseEvent e) {
configureGraphicsContext(gc);
gc.beginPath();
gc.moveTo(e.getX(), e.getY());
gc.stroke();
}
public void handleMouseReleased(GraphicsContext gc, MouseEvent e) {
currentOpacity = START_OPACITY;
gc.closePath();
}
public void handleMouseDragged(GraphicsContext gc, MouseEvent e) {
currentOpacity = Math.max(0, currentOpacity - OPACITY_MODIFIER);
configureGraphicsContext(gc);
gc.lineTo(e.getX(), e.getY());
gc.stroke();
}
}
Anyone with some tips on how to get closer to the real thing?
It all depends on what you're trying to achieve. Personally I would use
an AnimationTimer
a customizable Brush (i. e. an Image) instead of a stroke, so you can specify size and hardness
a line drawing algorithm (like Bresenham) to connect the previous mouse location with the current one to get a full line between points
A quick example with a simple drawing algorithm:
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class Main extends Application {
private static double SCENE_WIDTH = 1280;
private static double SCENE_HEIGHT = 720;
static Random random = new Random();
Canvas canvas;
GraphicsContext graphicsContext;
AnimationTimer loop;
Point2D mouseLocation = new Point2D( 0, 0);
boolean mousePressed = false;
Point2D prevMouseLocation = new Point2D( 0, 0);
Scene scene;
Image brush = createBrush( 30.0, Color.CHOCOLATE);
double brushWidthHalf = brush.getWidth() / 2.0;
double brushHeightHalf = brush.getHeight() / 2.0;
#Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
canvas = new Canvas( SCENE_WIDTH, SCENE_HEIGHT);
graphicsContext = canvas.getGraphicsContext2D();
Pane layerPane = new Pane();
layerPane.getChildren().addAll(canvas);
root.setCenter(layerPane);
scene = new Scene(root, SCENE_WIDTH, SCENE_HEIGHT);
primaryStage.setScene(scene);
primaryStage.show();
addListeners();
startAnimation();
}
private void startAnimation() {
loop = new AnimationTimer() {
#Override
public void handle(long now) {
if( mousePressed) {
// try this
// graphicsContext.drawImage( brush, mouseLocation.getX() - brushWidthHalf, mouseLocation.getY() - brushHeightHalf);
// then this
bresenhamLine( prevMouseLocation.getX(), prevMouseLocation.getY(), mouseLocation.getX(), mouseLocation.getY());
}
prevMouseLocation = new Point2D( mouseLocation.getX(), mouseLocation.getY());
}
};
loop.start();
}
// https://de.wikipedia.org/wiki/Bresenham-Algorithmus
private void bresenhamLine(double x0, double y0, double x1, double y1)
{
double dx = Math.abs(x1-x0), sx = x0<x1 ? 1. : -1.;
double dy = -Math.abs(y1-y0), sy = y0<y1 ? 1. : -1.;
double err = dx+dy, e2; /* error value e_xy */
while( true){
graphicsContext.drawImage( brush, x0 - brushWidthHalf, y0 - brushHeightHalf);
if (x0==x1 && y0==y1) break;
e2 = 2.*err;
if (e2 > dy) { err += dy; x0 += sx; } /* e_xy+e_x > 0 */
if (e2 < dx) { err += dx; y0 += sy; } /* e_xy+e_y < 0 */
}
}
private void addListeners() {
scene.addEventFilter(MouseEvent.ANY, e -> {
mouseLocation = new Point2D(e.getX(), e.getY());
mousePressed = e.isPrimaryButtonDown();
});
}
public static Image createImage(Node node) {
WritableImage wi;
SnapshotParameters parameters = new SnapshotParameters();
parameters.setFill(Color.TRANSPARENT);
int imageWidth = (int) node.getBoundsInLocal().getWidth();
int imageHeight = (int) node.getBoundsInLocal().getHeight();
wi = new WritableImage(imageWidth, imageHeight);
node.snapshot(parameters, wi);
return wi;
}
public static Image createBrush( double radius, Color color) {
// create gradient image with given color
Circle brush = new Circle(radius);
RadialGradient gradient1 = new RadialGradient(0, 0, 0, 0, radius, false, CycleMethod.NO_CYCLE, new Stop(0, color.deriveColor(1, 1, 1, 0.3)), new Stop(1, color.deriveColor(1, 1, 1, 0)));
brush.setFill(gradient1);
// create image
return createImage(brush);
}
public static void main(String[] args) {
launch(args);
}
}
Of course you can extend this with e. g.
multiple layers
JavaFX's blend modes on layer and graphicscontext level
to simulate force I'd use a paint delay (eg 200 ms) and a buffer for the mouse locations and let the opacity depend on whether the mouse is still pressed or not
smooth the lines by using bezier curves
...
Example with Brush variations when you start painting:
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class Main extends Application {
private static double SCENE_WIDTH = 1280;
private static double SCENE_HEIGHT = 720;
static Random random = new Random();
Canvas canvas;
GraphicsContext graphicsContext;
AnimationTimer loop;
Point2D mouseLocation = new Point2D( 0, 0);
boolean mousePressed = false;
Point2D prevMouseLocation = new Point2D( 0, 0);
Scene scene;
double brushMaxSize = 30;
Image brush = createBrush( brushMaxSize, Color.CHOCOLATE);
double brushWidthHalf = brush.getWidth() / 2.0;
double brushHeightHalf = brush.getHeight() / 2.0;
double pressure = 0;
double pressureDelay = 0.04;
private Image[] brushVariations = new Image[256];
#Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
canvas = new Canvas( SCENE_WIDTH, SCENE_HEIGHT);
for( int i=0; i < brushVariations.length; i++) {
double size = (brushMaxSize - 1) / (double) brushVariations.length * (double) i + 1;
brushVariations[i] = createBrush( size, Color.CHOCOLATE);
}
graphicsContext = canvas.getGraphicsContext2D();
Pane layerPane = new Pane();
layerPane.getChildren().addAll(canvas);
root.setCenter(layerPane);
scene = new Scene(root, SCENE_WIDTH, SCENE_HEIGHT);
primaryStage.setScene(scene);
primaryStage.show();
addListeners();
startAnimation();
}
private void startAnimation() {
loop = new AnimationTimer() {
#Override
public void handle(long now) {
if( mousePressed) {
// try this
// graphicsContext.drawImage( brush, mouseLocation.getX() - brushWidthHalf, mouseLocation.getY() - brushHeightHalf);
// then this
bresenhamLine( prevMouseLocation.getX(), prevMouseLocation.getY(), mouseLocation.getX(), mouseLocation.getY());
pressure += pressureDelay;
if( pressure > 1) {
pressure = 1;
}
} else {
pressure = 0;
}
prevMouseLocation = new Point2D( mouseLocation.getX(), mouseLocation.getY());
}
};
loop.start();
}
// https://de.wikipedia.org/wiki/Bresenham-Algorithmus
private void bresenhamLine(double x0, double y0, double x1, double y1)
{
double dx = Math.abs(x1-x0), sx = x0<x1 ? 1. : -1.;
double dy = -Math.abs(y1-y0), sy = y0<y1 ? 1. : -1.;
double err = dx+dy, e2; /* error value e_xy */
while( true){
int variation = (int) (pressure * (brushVariations.length - 1));
Image brushVariation = brushVariations[ variation ];
graphicsContext.setGlobalAlpha(pressure);
graphicsContext.drawImage( brushVariation, x0 - brushWidthHalf, y0 - brushHeightHalf);
if (x0==x1 && y0==y1) break;
e2 = 2.*err;
if (e2 > dy) { err += dy; x0 += sx; } /* e_xy+e_x > 0 */
if (e2 < dx) { err += dx; y0 += sy; } /* e_xy+e_y < 0 */
}
}
private void addListeners() {
scene.addEventFilter(MouseEvent.ANY, e -> {
mouseLocation = new Point2D(e.getX(), e.getY());
mousePressed = e.isPrimaryButtonDown();
});
}
public static Image createImage(Node node) {
WritableImage wi;
SnapshotParameters parameters = new SnapshotParameters();
parameters.setFill(Color.TRANSPARENT);
int imageWidth = (int) node.getBoundsInLocal().getWidth();
int imageHeight = (int) node.getBoundsInLocal().getHeight();
wi = new WritableImage(imageWidth, imageHeight);
node.snapshot(parameters, wi);
return wi;
}
public static Image createBrush( double radius, Color color) {
// create gradient image with given color
Circle brush = new Circle(radius);
RadialGradient gradient1 = new RadialGradient(0, 0, 0, 0, radius, false, CycleMethod.NO_CYCLE, new Stop(0, color.deriveColor(1, 1, 1, 0.3)), new Stop(1, color.deriveColor(1, 1, 1, 0)));
brush.setFill(gradient1);
// create image
return createImage(brush);
}
public static void main(String[] args) {
launch(args);
}
}
Example with variation for limiting the brush length
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ColorPicker;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class Main extends Application {
private static double SCENE_WIDTH = 1280;
private static double SCENE_HEIGHT = 720;
Canvas canvas;
GraphicsContext graphicsContext;
AnimationTimer loop;
Point2D mouseLocation = new Point2D(0, 0);
boolean mousePressed = false;
Point2D prevMouseLocation = new Point2D(0, 0);
Scene scene;
double brushMaxSize = 30;
double pressure = 0;
double pressureDelay = 0.04;
double pressureDirection = 1;
double strokeTimeMax = 1;
double strokeTime = 0;
double strokeTimeDelay = 0.07;
private Image[] brushVariations = new Image[256];
ColorPicker colorPicker = new ColorPicker();
#Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
canvas = new Canvas(SCENE_WIDTH, SCENE_HEIGHT);
graphicsContext = canvas.getGraphicsContext2D();
graphicsContext.setFill(Color.WHITE);
graphicsContext.fillRect(0, 0, SCENE_WIDTH, SCENE_HEIGHT);
Pane layerPane = new Pane();
layerPane.getChildren().addAll(canvas);
colorPicker.setValue(Color.CHOCOLATE);
colorPicker.setOnAction(e -> {
createBrushVariations();
});
root.setCenter(layerPane);
root.setTop(colorPicker);
scene = new Scene(root, SCENE_WIDTH, SCENE_HEIGHT, Color.WHITE);
primaryStage.setScene(scene);
primaryStage.show();
createBrushVariations();
addListeners();
startAnimation();
}
private void createBrushVariations() {
for (int i = 0; i < brushVariations.length; i++) {
double size = (brushMaxSize - 1) / (double) brushVariations.length * (double) i + 1;
brushVariations[i] = createBrush(size, colorPicker.getValue());
}
}
private void startAnimation() {
loop = new AnimationTimer() {
#Override
public void handle(long now) {
if (mousePressed) {
// try this
// graphicsContext.drawImage( brush, mouseLocation.getX() -
// brushWidthHalf, mouseLocation.getY() - brushHeightHalf);
// then this
bresenhamLine(prevMouseLocation.getX(), prevMouseLocation.getY(), mouseLocation.getX(), mouseLocation.getY());
// increasing or decreasing
strokeTime += strokeTimeDelay * pressureDirection;
// invert direction
if (strokeTime > strokeTimeMax) {
pressureDirection = -1;
}
// while still
if (strokeTime > 0) {
pressure += pressureDelay * pressureDirection;
// clamp value of pressure to be [0,1]
if (pressure > 1) {
pressure = 1;
} else if (pressure < 0) {
pressure = 0;
}
} else {
pressure = 0;
}
} else {
pressure = 0;
pressureDirection = 1;
strokeTime = 0;
}
prevMouseLocation = new Point2D(mouseLocation.getX(), mouseLocation.getY());
}
};
loop.start();
}
// https://de.wikipedia.org/wiki/Bresenham-Algorithmus
private void bresenhamLine(double x0, double y0, double x1, double y1) {
double dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1. : -1.;
double dy = -Math.abs(y1 - y0), sy = y0 < y1 ? 1. : -1.;
double err = dx + dy, e2; /* error value e_xy */
while (true) {
int variation = (int) (pressure * (brushVariations.length - 1));
Image brushVariation = brushVariations[variation];
graphicsContext.setGlobalAlpha(pressure);
graphicsContext.drawImage(brushVariation, x0 - brushVariation.getWidth() / 2.0, y0 - brushVariation.getHeight() / 2.0);
if (x0 == x1 && y0 == y1)
break;
e2 = 2. * err;
if (e2 > dy) {
err += dy;
x0 += sx;
} /* e_xy+e_x > 0 */
if (e2 < dx) {
err += dx;
y0 += sy;
} /* e_xy+e_y < 0 */
}
}
private void addListeners() {
canvas.addEventFilter(MouseEvent.ANY, e -> {
mouseLocation = new Point2D(e.getX(), e.getY());
mousePressed = e.isPrimaryButtonDown();
});
}
public static Image createImage(Node node) {
WritableImage wi;
SnapshotParameters parameters = new SnapshotParameters();
parameters.setFill(Color.TRANSPARENT);
int imageWidth = (int) node.getBoundsInLocal().getWidth();
int imageHeight = (int) node.getBoundsInLocal().getHeight();
wi = new WritableImage(imageWidth, imageHeight);
node.snapshot(parameters, wi);
return wi;
}
public static Image createBrush(double radius, Color color) {
// create gradient image with given color
Circle brush = new Circle(radius);
RadialGradient gradient1 = new RadialGradient(0, 0, 0, 0, radius, false, CycleMethod.NO_CYCLE, new Stop(0, color.deriveColor(1, 1, 1, 0.3)), new Stop(1, color.deriveColor(1, 1, 1, 0)));
brush.setFill(gradient1);
// create image
return createImage(brush);
}
public static void main(String[] args) {
launch(args);
}
}
This is how it looks like:
or using different colors, I added a color picker in the last example:
Related
I am trying to create a first person camera in JavaFX based on bindings. The camera and the actual position both work perfectly. The only problem is that they don’t match! As you can see in the picture, the actual position (red box) is in the middle of the circle, but the camera is outside. How can I change that? What did I do wrong?
The Player class handles the PerspectiveCamera.
package game;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.PerspectiveCamera;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.robot.Robot;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
public class Player extends Character {
private static final Robot ROBOT = new Robot();
private DoubleProperty relativeCenterX = new SimpleDoubleProperty();
private DoubleProperty relativeCenterY = new SimpleDoubleProperty();
protected PerspectiveCamera camera = new PerspectiveCamera();
protected Rotate xAxis = new Rotate(0, 250, 0, 0, Rotate.Y_AXIS);
protected Rotate yAxis = new Rotate(0, 0, 250, 0, Rotate.X_AXIS);
protected Translate translate = new Translate();
protected DoubleProperty centerX = new SimpleDoubleProperty();
protected DoubleProperty centerY = new SimpleDoubleProperty();
#SuppressWarnings("exports")
public Player(Stage stage) {
camera.getTransforms().addAll(xAxis, yAxis);
centerX.bind(stage.widthProperty().divide(2));
centerY.bind(stage.heightProperty().divide(2));
relativeCenterX.bind(stage.xProperty().add(centerX));
relativeCenterY.bind(stage.yProperty().add(centerY));
camera.translateXProperty().bind(posX.subtract(centerX));
camera.translateYProperty().bind(posZ);
camera.translateZProperty().bind(posY.subtract(centerY));
xAxis.angleProperty().bind(viewX.subtract(90));
yAxis.angleProperty().bind(viewY);
translate.xProperty().bind(posX);
translate.zProperty().bind(posY);
translate.yProperty().bind(posZ);
}
#SuppressWarnings("exports")
public EventHandler<KeyEvent> getKeyHandle() {
return e -> {
switch (e.getCode()) {
case A:
view(-1, 0);
break;
case D:
view(1, 0);
break;
case W:
move(1, 1, 0);
break;
case S:
move(-1, -1, 0);
break;
case SPACE:
move(0, 0, 10);
break;
case F:
move(0, 0, -10);
break;
default:
break;
}
};
}
#SuppressWarnings("exports")
public EventHandler<MouseEvent> getMouseHandle() {
return e -> {
view(e.getSceneX() - centerX.doubleValue(), centerY.doubleValue() - e.getSceneY());
Platform.runLater(() -> {
ROBOT.mouseMove(relativeCenterX.intValue(), relativeCenterY.intValue());
});
};
}
#SuppressWarnings("exports")
public PerspectiveCamera getPespectiveCamera() {
return camera;
}
}
The Character class calculates position and view.
package game;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public abstract class Character {
protected DoubleProperty posX = new SimpleDoubleProperty();
protected DoubleProperty posY = new SimpleDoubleProperty();
protected DoubleProperty posZ = new SimpleDoubleProperty();
protected DoubleProperty viewX = new SimpleDoubleProperty();
protected DoubleProperty viewY = new SimpleDoubleProperty();
protected DoubleProperty speed = new SimpleDoubleProperty(10);
public void move(double x, double y, double z) {
double fX = Math.cos(Math.toRadians(viewX.get()));
double fY = -Math.sin(Math.toRadians(viewX.get()));
double fZ = 1;
posX.set(posX.get() + fX * x * speed.get());
posY.set(posY.get() + fY * y * speed.get());
posZ.set(posZ.get() + fZ * z);
}
public void view(double x, double y) {
viewX.set(viewX.get() + x);
viewY.set(viewY.get() + y);
}
#SuppressWarnings("exports")
public DoubleProperty posXPorperty() {
return posX;
}
#SuppressWarnings("exports")
public DoubleProperty posYPorperty() {
return posY;
}
#SuppressWarnings("exports")
public DoubleProperty posZPorperty() {
return posZ;
}
#SuppressWarnings("exports")
public DoubleProperty viewXPorperty() {
return viewX;
}
#SuppressWarnings("exports")
public DoubleProperty viewYPorperty() {
return viewY;
}
}
My Application, which shows the total graphical content.
package graphics;
import game.Player;
import javafx.application.Application;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class GameStage extends Application implements Runnable {
#Override
public void run() {
launch();
}
#SuppressWarnings("exports")
#Override
public void start(Stage stage) throws Exception {
BorderPane pane = new BorderPane();
Scene scene = new Scene(pane, 500, 500);
Group content = new Group(), map = new Group();
ContentScene subscene = new ContentScene(content, map, 500, 500);
subscene.widthProperty().bind(scene.widthProperty());
subscene.heightProperty().bind(scene.heightProperty());
pane.getChildren().add(subscene);
pane.setBottom(map);
Player player = new Player(stage);
Box box = new Box(50, 50, 50);
box.translateXProperty().bind(player.posXPorperty());
box.translateYProperty().bind(player.posZPorperty());
box.translateZProperty().bind(player.posYPorperty());
box.rotateProperty().bind(player.viewXPorperty());
box.setMaterial(new PhongMaterial(Color.RED));
content.getChildren().add(box);
Rectangle rectangle = new Rectangle(5, 5);
rectangle.translateXProperty().bind(player.posXPorperty().divide(10));
rectangle.translateYProperty().bind(player.posYPorperty().divide(10));
rectangle.setFill(Color.RED);
map.getChildren().add(rectangle);
subscene.setCamera(player.getPespectiveCamera());
scene.addEventHandler(KeyEvent.KEY_PRESSED, player.getKeyHandle());
scene.addEventHandler(MouseEvent.MOUSE_MOVED, player.getMouseHandle());
scene.setFill(Color.BLACK);
Cursor cursor = Cursor.CROSSHAIR;
scene.setCursor(cursor);
stage.addEventHandler(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode() != KeyCode.F11) {
return;
}
if (stage.isFullScreen()) {
stage.setFullScreen(false);
} else {
stage.setFullScreen(true);
}
});
stage.setAlwaysOnTop(true);
stage.setScene(scene);
stage.show();
}
private class ContentScene extends SubScene {
public ContentScene(Group content, Group map, double width, double height) {
super(content, width, height, true, SceneAntialiasing.BALANCED);
PhongMaterial material = new PhongMaterial(Color.AQUA);
for (int v = 0; v < 3_600; v += 180) {
for (int y = 0; y < 500; y += 100) {
Box box = new Box(50, 50, 50);
box.setTranslateX(Math.sin(v / 10) * 1_000);
box.setTranslateY(y);
box.setTranslateZ(Math.cos(v / 10) * 1_000);
box.setMaterial(material);
content.getChildren().add(box);
Rectangle rectangle = new Rectangle(5, 5);
rectangle.translateXProperty().bind(box.translateXProperty().divide(10));
rectangle.translateYProperty().bind(box.translateZProperty().divide(10));
rectangle.setFill(Color.AQUA);
map.getChildren().add(rectangle);
}
}
}
}
}
Thanks to Thomas, I was able to solve the problem. The code now loks like this:
package graphics;
import game.Player;
import javafx.application.Application;
import javafx.scene.AmbientLight;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class GameStage extends Application implements Runnable {
#Override
public void run() {
launch();
}
#SuppressWarnings("exports")
#Override
public void start(Stage stage) throws Exception {
// Parents
BorderPane pane = new BorderPane();
Group content = new Group(new AmbientLight()), map = new Group();
Pane stackpane = new Pane(map);
// Scenes
Scene scene = new Scene(pane, 500, 500);
SubScene contentSubscene = new SubScene(content, 500, 500, true, SceneAntialiasing.BALANCED);
contentSubscene.widthProperty().bind(scene.widthProperty());
contentSubscene.heightProperty().bind(scene.heightProperty());
SubScene minimapSubscene = new SubScene(stackpane, 256, 256);
minimapSubscene.setFill(Color.DARKGREY);
pane.getChildren().add(contentSubscene);
pane.setBottom(minimapSubscene);
// Create Player
Player player = new Player(stage);
Rectangle currentPosition = new Rectangle(5, 5);
currentPosition.layoutXProperty().bind(minimapSubscene.widthProperty().divide(2));
currentPosition.layoutYProperty().bind(minimapSubscene.heightProperty().divide(2));
currentPosition.setFill(Color.RED);
stackpane.getChildren().add(currentPosition);
map.layoutXProperty().bind(player.posXPorperty().divide(-10).add(minimapSubscene.widthProperty().divide(2)));
map.layoutYProperty().bind(player.posYPorperty().divide(-10).add(minimapSubscene.heightProperty().divide(2)));
// Create Box in
PhongMaterial material = new PhongMaterial(Color.AQUA);
for (int v = 0; v < 3_600; v += 180) {
for (int y = 0; y < 500; y += 100) {
Box box = new Box(50, 50, 50);
box.setTranslateX(Math.sin(v / 10) * 1_000);
box.setTranslateY(y);
box.setTranslateZ(Math.cos(v / 10) * 1_000);
box.setMaterial(material);
content.getChildren().add(box);
Rectangle boxPosition = new Rectangle(5, 5);
boxPosition.translateXProperty().bind(box.translateXProperty().divide(10));
boxPosition.translateYProperty().bind(box.translateZProperty().divide(10));
boxPosition.setFill(Color.AQUA);
map.getChildren().add(boxPosition);
}
}
contentSubscene.setCamera(player.getPespectiveCamera());
scene.addEventHandler(KeyEvent.KEY_PRESSED, player.getKeyHandle());
scene.addEventHandler(MouseEvent.MOUSE_MOVED, player.getMouseHandle());
scene.setCursor(Cursor.CROSSHAIR);
scene.setFill(Color.WHITE);
stage.addEventHandler(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode() != KeyCode.F11) {
return;
}
if (stage.isFullScreen()) {
stage.setFullScreen(false);
} else {
stage.setFullScreen(true);
}
});
stage.setAlwaysOnTop(true);
stage.setScene(scene);
stage.show();
}
}
package game;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public abstract class Character {
protected DoubleProperty posX = new SimpleDoubleProperty();
protected DoubleProperty posY = new SimpleDoubleProperty();
protected DoubleProperty posZ = new SimpleDoubleProperty();
protected DoubleProperty viewX = new SimpleDoubleProperty();
protected DoubleProperty viewY = new SimpleDoubleProperty();
protected DoubleProperty speed = new SimpleDoubleProperty(10);
public void move(double x, double y, double z) {
double fX = Math.cos(Math.toRadians(viewX.get()));
double fY = -Math.sin(Math.toRadians(viewX.get()));
double fZ = 1;
posX.set(posX.get() + fX * x * speed.get());
posY.set(posY.get() + fY * y * speed.get());
posZ.set(posZ.get() + fZ * z);
}
public void view(double x, double y) {
viewX.set(viewX.get() + x);
viewY.set(viewY.get() + y);
}
#SuppressWarnings("exports")
public DoubleProperty posXPorperty() {
return posX;
}
#SuppressWarnings("exports")
public DoubleProperty posYPorperty() {
return posY;
}
#SuppressWarnings("exports")
public DoubleProperty posZPorperty() {
return posZ;
}
#SuppressWarnings("exports")
public DoubleProperty viewXPorperty() {
return viewX;
}
#SuppressWarnings("exports")
public DoubleProperty viewYPorperty() {
return viewY;
}
}
package game;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.PerspectiveCamera;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.robot.Robot;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
public class Player extends Character {
private static final Robot ROBOT = new Robot();
private DoubleProperty relativeCenterX = new SimpleDoubleProperty();
private DoubleProperty relativeCenterY = new SimpleDoubleProperty();
protected PerspectiveCamera camera = new PerspectiveCamera(true);
protected Rotate xAxis = new Rotate(0, 250, 0, 0, Rotate.Y_AXIS);
protected Rotate yAxis = new Rotate(0, 0, 250, 0, Rotate.X_AXIS);
protected DoubleProperty centerX = new SimpleDoubleProperty();
protected DoubleProperty centerY = new SimpleDoubleProperty();
#SuppressWarnings("exports")
public Player(Stage stage) {
camera.getTransforms().addAll(xAxis, yAxis);
camera.setFieldOfView((40 + 62) / 2);
camera.setNearClip(0.1);
camera.setFarClip(100000);
camera.setVerticalFieldOfView(true);
centerX.bind(stage.widthProperty().divide(2));
centerY.bind(stage.heightProperty().divide(2));
relativeCenterX.bind(stage.xProperty().add(centerX));
relativeCenterY.bind(stage.yProperty().add(centerY));
xAxis.angleProperty().bind(viewX.subtract(90));
yAxis.angleProperty().bind(viewY);
camera.translateXProperty().bind(posX);
camera.translateZProperty().bind(posY);
camera.translateYProperty().bind(posZ);
}
#SuppressWarnings("exports")
public EventHandler<KeyEvent> getKeyHandle() {
return e -> {
switch (e.getCode()) {
case A:
view(-1, 0);
break;
case D:
view(1, 0);
break;
case W:
move(1, 1, 0);
break;
case S:
move(-1, -1, 0);
break;
case SPACE:
move(0, 0, 10);
break;
case F:
move(0, 0, -10);
break;
default:
break;
}
};
}
#SuppressWarnings("exports")
public EventHandler<MouseEvent> getMouseHandle() {
return e -> {
view(e.getSceneX() - centerX.doubleValue(), centerY.doubleValue() - e.getSceneY());
Platform.runLater(() -> {
ROBOT.mouseMove(relativeCenterX.intValue(), relativeCenterY.intValue());
});
};
}
#SuppressWarnings("exports")
public PerspectiveCamera getPespectiveCamera() {
return camera;
}
}
I created a star plotting application to plot labels for a 3d star plotter so that the labels are mapped local to scene. I wanted to do this so that whenever I rotate the starfield, the labels are always facing forward and don't rotate with the individual stars (annoying because they can't be read).
Now this works very well until I added a control panel above it. In the application below, if the control panel is not present, then labels track with the position of the stars. But with the control panel, the labels are y-offset by the size of the control panel height.
The issue is that the "mousePosY = me.getSceneY();" returns the mouse position of the scene itself and not the defined subScene. I had thought that the "subScene.setOnMouseDragged((MouseEvent me) -> {" would have given me the position of the mouse relative to the subScene and not the scene.
Is there any way to fix this so that the returned X,Y position is for the subScene only?
The following code is a very cutdown version of my application that shows the actual issue itself.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Point3D;
import javafx.scene.*;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Sphere;
import javafx.scene.text.Font;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Random;
import static org.fxyz3d.geometry.MathUtils.clamp;
/**
* example for flat labels
*/
#Slf4j
public class StarFieldExample extends Application {
public static final int SCALE_X = 510;
public static final int SCALE_Y = 540;
public static final int SCALE_Z = 0;
final double sceneWidth = 600;
final double sceneHeight = 600;
private double mousePosX;
private double mousePosY;
private double mouseOldX;
private double mouseOldY;
private double mouseDeltaX;
private double mouseDeltaY;
private final Font font = new Font("arial", 10);
// We'll use custom Rotate transforms to manage the coordinate conversions
private final Rotate rotateX = new Rotate(0, Rotate.X_AXIS);
private final Rotate rotateY = new Rotate(0, Rotate.Y_AXIS);
private final Rotate rotateZ = new Rotate(0, Rotate.Z_AXIS);
private final Group root = new Group();
private final Group world = new Group(); //all 3D nodes in scene
private final Group labelGroup = new Group(); //all generic 3D labels
//All shapes and labels linked via hash for easy update during camera movement
private final HashMap<Node, Label> shape3DToLabel = new HashMap<>();
private SubScene subScene;
////// support
private final Random random = new Random();
private final static double RADIUS_MAX = 7;
private final static double X_MAX = 300;
private final static double Y_MAX = 300;
private final static double Z_MAX = 300;
private final Label scaleLabel = new Label("Scale: 5 ly");
public Pane createStarField() {
// attach our custom rotation transforms so we can update the labels dynamically
world.getTransforms().addAll(rotateX, rotateY, rotateZ);
subScene = new SubScene(world, sceneWidth, sceneHeight, true, SceneAntialiasing.BALANCED);
subScene.setFill(Color.BLACK);
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setNearClip(0.1);
camera.setFarClip(10000.0);
camera.setTranslateZ(-1000);
subScene.setCamera(camera);
Group sceneRoot = new Group(subScene);
sceneRoot.getChildren().add(labelGroup);
generateRandomStars(5);
handleMouseEvents();
// add to the 2D portion of this component
Pane pane = new Pane();
pane.setPrefSize(sceneWidth, sceneHeight);
pane.setMaxSize(Pane.USE_COMPUTED_SIZE, Pane.USE_COMPUTED_SIZE);
pane.setMinSize(Pane.USE_COMPUTED_SIZE, Pane.USE_COMPUTED_SIZE);
pane.setBackground(Background.EMPTY);
pane.getChildren().add(sceneRoot);
pane.setPickOnBounds(true);
subScene.widthProperty().bind(pane.widthProperty());
subScene.heightProperty().bind(pane.heightProperty());
Platform.runLater(this::updateLabels);
return (pane);
}
private void handleMouseEvents() {
subScene.setOnMousePressed((MouseEvent me) -> {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
}
);
subScene.setOnMouseDragged((MouseEvent me) -> {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
double modifier = 5.0;
double modifierFactor = 0.1;
if (me.isPrimaryButtonDown()) {
if (me.isAltDown()) { //roll
rotateZ.setAngle(((rotateZ.getAngle() + mouseDeltaX * modifierFactor * modifier * 2.0) % 360 + 540) % 360 - 180); // +
} else {
rotateY.setAngle(((rotateY.getAngle() + mouseDeltaX * modifierFactor * modifier * 2.0) % 360 + 540) % 360 - 180); // +
rotateX.setAngle(
clamp(
(((rotateX.getAngle() - mouseDeltaY * modifierFactor * modifier * 2.0) % 360 + 540) % 360 - 180),
-60,
60
)
); // -
}
}
updateLabels();
}
);
}
public void generateRandomStars(int numberStars) {
for (int i = 0; i < numberStars; i++) {
double radius = random.nextDouble() * RADIUS_MAX;
Color color = randomColor();
double x = random.nextDouble() * X_MAX * 2 / 3 * (random.nextBoolean() ? 1 : -1);
double y = random.nextDouble() * Y_MAX * 2 / 3 * (random.nextBoolean() ? 1 : -1);
double z = random.nextDouble() * Z_MAX * 2 / 3 * (random.nextBoolean() ? 1 : -1);
String labelText = "Star " + i;
boolean fadeFlag = random.nextBoolean();
createSphereLabel(radius, x, y, z, color, labelText, fadeFlag);
}
//Add to hashmap so updateLabels() can manage the label position
scaleLabel.setFont(new Font("Arial", 15));
scaleLabel.setTextFill(Color.WHEAT);
scaleLabel.setTranslateX(SCALE_X);
scaleLabel.setTranslateY(SCALE_Y);
scaleLabel.setTranslateZ(SCALE_Z);
labelGroup.getChildren().add(scaleLabel);
log.info("shapes:{}", shape3DToLabel.size());
}
private Color randomColor() {
int r = random.nextInt(255);
int g = random.nextInt(255);
int b = random.nextInt(255);
return Color.rgb(r, g, b);
}
private void createSphereLabel(double radius, double x, double y, double z, Color color, String labelText, boolean fadeFlag) {
Sphere sphere = new Sphere(radius);
sphere.setTranslateX(x);
sphere.setTranslateY(y);
sphere.setTranslateZ(z);
sphere.setMaterial(new PhongMaterial(color));
//add our nodes to the group that will later be added to the 3D scene
world.getChildren().add(sphere);
Label label = new Label(labelText);
label.setTextFill(color);
label.setFont(font);
labelGroup.getChildren().add(label);
//Add to hashmap so updateLabels() can manage the label position
shape3DToLabel.put(sphere, label);
}
private void updateLabels() {
shape3DToLabel.forEach((node, label) -> {
Point3D coordinates = node.localToScene(Point3D.ZERO, true);
//Clipping Logic
//if coordinates are outside of the scene it could
//stretch the screen so don't transform them
double x = coordinates.getX();
double y = coordinates.getY();
// is it left of the view?
if (x < 0) {
x = 0;
}
// is it right of the view?
if ((x + label.getWidth() + 5) > subScene.getWidth()) {
x = subScene.getWidth() - (label.getWidth() + 5);
}
// is it above the view?
if (y < 0) {
y = 0;
}
// is it below the view
if ((y + label.getHeight()) > subScene.getHeight()) {
y = subScene.getHeight() - (label.getHeight() + 5);
}
//update the local transform of the label.
label.getTransforms().setAll(new Translate(x, y));
});
scaleLabel.setTranslateX(SCALE_X);
scaleLabel.setTranslateY(SCALE_Y);
scaleLabel.setTranslateZ(SCALE_Z);
}
//////////////////////////////////
#Override
public void start(Stage primaryStage) throws Exception {
Pane controls = createControls();
Pane pane = createStarField();
VBox vBox = new VBox(
controls,
pane
);
root.getChildren().add(vBox);
Scene scene = new Scene(root, sceneWidth, sceneHeight - 40);
primaryStage.setTitle("2D Labels over 3D SubScene");
primaryStage.setScene(scene);
primaryStage.show();
}
private VBox createControls() {
VBox controls = new VBox(10, new Button("Button"));
controls.setPadding(new Insets(10));
return controls;
}
public static void main(String[] args) {
launch(args);
}
}
I broke this down into a simpler problem relating mouse offset, solved that, and then worked that solution into here.
The key issue is that I assumed that the mouse event (x,y) positions would be zero offset to the SubScene, and in reality, they are relative to the containing scene. I had to calculate the bounds of the component relative to the parent. Having the actual location of the SubScene in the layout of the larger application allows me to calculate the corrected (x,y) positions.
Here is the answer:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Point3D;
import javafx.scene.*;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Sphere;
import javafx.scene.text.Font;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import static org.fxyz3d.geometry.MathUtils.clamp;
/**
* example for flat labels
*/
#Slf4j
public class StarFieldExample extends Application {
public static final int SCALE_X = 510;
public static final int SCALE_Y = 540;
public static final int SCALE_Z = 0;
final double sceneWidth = 600;
final double sceneHeight = 600;
private double mousePosX;
private double mousePosY;
private double mouseOldX;
private double mouseOldY;
private double mouseDeltaX;
private double mouseDeltaY;
private final Font font = new Font("arial", 10);
// We'll use custom Rotate transforms to manage the coordinate conversions
private final Rotate rotateX = new Rotate(0, Rotate.X_AXIS);
private final Rotate rotateY = new Rotate(0, Rotate.Y_AXIS);
private final Rotate rotateZ = new Rotate(0, Rotate.Z_AXIS);
private final Group root = new Group();
private final Group world = new Group(); //all 3D nodes in scene
private final Group labelGroup = new Group(); //all generic 3D labels
//All shapes and labels linked via hash for easy update during camera movement
private final Map<Node, Label> shape3DToLabel = new HashMap<>();
private SubScene subScene;
////// support
private final Random random = new Random();
private final static double RADIUS_MAX = 7;
private final static double X_MAX = 300;
private final static double Y_MAX = 300;
private final static double Z_MAX = 300;
Pane pane;
private final Label scaleLabel = new Label("Scale: 5 ly");
public Pane createStarField() {
PerspectiveCamera camera = setupPerspectiveCamera();
// attach our custom rotation transforms so we can update the labels dynamically
world.getTransforms().addAll(rotateX, rotateY, rotateZ);
subScene = new SubScene(world, sceneWidth, sceneHeight, true, SceneAntialiasing.BALANCED);
subScene.setFill(Color.BLACK);
subScene.setCamera(camera);
Group sceneRoot = new Group(subScene);
sceneRoot.getChildren().add(labelGroup);
generateRandomStars(5);
// add to the 2D portion of this component
pane = new Pane();
pane.setPrefSize(sceneWidth, sceneHeight);
pane.setMaxSize(Pane.USE_COMPUTED_SIZE, Pane.USE_COMPUTED_SIZE);
pane.setMinSize(Pane.USE_COMPUTED_SIZE, Pane.USE_COMPUTED_SIZE);
pane.setBackground(Background.EMPTY);
pane.getChildren().add(sceneRoot);
pane.setPickOnBounds(true);
subScene.widthProperty().bind(pane.widthProperty());
subScene.heightProperty().bind(pane.heightProperty());
Platform.runLater(this::updateLabels);
handleMouseEvents();
return (pane);
}
#NotNull
private PerspectiveCamera setupPerspectiveCamera() {
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setNearClip(0.1);
camera.setFarClip(10000.0);
camera.setTranslateZ(-1000);
return camera;
}
private void handleMouseEvents() {
subScene.setOnMousePressed((MouseEvent me) -> {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
}
);
subScene.setOnMouseDragged((MouseEvent me) -> {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
double modifier = 5.0;
double modifierFactor = 0.1;
if (me.isPrimaryButtonDown()) {
if (me.isAltDown()) { //roll
rotateZ.setAngle(((rotateZ.getAngle() + mouseDeltaX * modifierFactor * modifier * 2.0) % 360 + 540) % 360 - 180); // +
} else {
rotateY.setAngle(((rotateY.getAngle() + mouseDeltaX * modifierFactor * modifier * 2.0) % 360 + 540) % 360 - 180); // +
rotateX.setAngle(
clamp(
(((rotateX.getAngle() - mouseDeltaY * modifierFactor * modifier * 2.0) % 360 + 540) % 360 - 180),
-60,
60
)
); // -
}
}
updateLabels();
}
);
}
private void updateLabels() {
shape3DToLabel.forEach((node, label) -> {
Point3D coordinates = node.localToScene(Point3D.ZERO, true);
//Clipping Logic
//if coordinates are outside of the scene it could
//stretch the screen so don't transform them
double xs = coordinates.getX();
double ys = coordinates.getY();
Bounds ofParent = pane.getBoundsInParent();
double x = xs - ofParent.getMinX();
double y = ys - ofParent.getMinY();
// is it left of the view?
if (x < 0) {
x = 0;
}
// is it right of the view?
if ((x + label.getWidth() + 5) > subScene.getWidth()) {
x = subScene.getWidth() - (label.getWidth() + 5);
}
// is it above the view?
if (y < 0) {
y = 0;
}
// is it below the view
if ((y + label.getHeight()) > subScene.getHeight()) {
y = subScene.getHeight() - (label.getHeight() + 5);
}
//update the local transform of the label.
label.getTransforms().setAll(new Translate(x, y));
});
scaleLabel.setTranslateX(SCALE_X);
scaleLabel.setTranslateY(SCALE_Y);
scaleLabel.setTranslateZ(SCALE_Z);
}
public void generateRandomStars(int numberStars) {
for (int i = 0; i < numberStars; i++) {
double radius = random.nextDouble() * RADIUS_MAX;
Color color = randomColor();
double x = random.nextDouble() * X_MAX * 2 / 3 * (random.nextBoolean() ? 1 : -1);
double y = random.nextDouble() * Y_MAX * 2 / 3 * (random.nextBoolean() ? 1 : -1);
double z = random.nextDouble() * Z_MAX * 2 / 3 * (random.nextBoolean() ? 1 : -1);
String labelText = "Star " + i;
boolean fadeFlag = random.nextBoolean();
createSphereLabel(radius, x, y, z, color, labelText, fadeFlag);
}
//Add to hashmap so updateLabels() can manage the label position
scaleLabel.setFont(new Font("Arial", 15));
scaleLabel.setTextFill(Color.WHEAT);
scaleLabel.setTranslateX(SCALE_X);
scaleLabel.setTranslateY(SCALE_Y);
scaleLabel.setTranslateZ(SCALE_Z);
labelGroup.getChildren().add(scaleLabel);
log.info("shapes:{}", shape3DToLabel.size());
}
private Color randomColor() {
int r = random.nextInt(255);
int g = random.nextInt(255);
int b = random.nextInt(255);
return Color.rgb(r, g, b);
}
private void createSphereLabel(double radius, double x, double y, double z, Color color, String labelText, boolean fadeFlag) {
Sphere sphere = new Sphere(radius);
sphere.setTranslateX(x);
sphere.setTranslateY(y);
sphere.setTranslateZ(z);
sphere.setMaterial(new PhongMaterial(color));
//add our nodes to the group that will later be added to the 3D scene
world.getChildren().add(sphere);
Label label = new Label(labelText);
label.setTextFill(color);
label.setFont(font);
labelGroup.getChildren().add(label);
//Add to hashmap so updateLabels() can manage the label position
shape3DToLabel.put(sphere, label);
}
//////////////////////////////////
#Override
public void start(Stage primaryStage) throws Exception {
Pane controls = createControls();
Pane pane = createStarField();
VBox vBox = new VBox(
controls,
pane
);
root.getChildren().add(vBox);
Scene scene = new Scene(root, sceneWidth, sceneHeight - 40);
primaryStage.setTitle("2D Labels over 3D SubScene");
primaryStage.setScene(scene);
primaryStage.show();
}
private VBox createControls() {
VBox controls = new VBox(10, new Button("Button"));
controls.setPadding(new Insets(10));
return controls;
}
public static void main(String[] args) {
launch(args);
}
}
I have a set of Nodes, Circles, on the stage.
I want to be able to click on one of them and 'select it' (just get a reference to it so I can move it around, change color etc.)
Pane root = new Pane();
root.getChildren().addAll( /* an array of Circle objects */ );
Scene scene = new Scene(root, 500, 500, BACKGROUND_COLOR);
scene.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
// how do I get which Circle I clicked on?
}
});
stage.setTitle(TITLE);
stage.setScene(scene);
stage.show();
I would simply register a listener with each circle itself. Then you already have the reference to the circle with which the listener was registered.
This example pushes the limit a little as to usability, because it has 10,000 circles shown all at once, but it demonstrates the technique:
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class GridOfCircles extends Application {
private static final PseudoClass SELECTED_P_C = PseudoClass.getPseudoClass("selected");
private final int numColumns = 100 ;
private final int numRows = 100 ;
private final double radius = 4 ;
private final double spacing = 2 ;
private final ObjectProperty<Circle> selectedCircle = new SimpleObjectProperty<>();
private final ObjectProperty<Point2D> selectedLocation = new SimpleObjectProperty<>();
#Override
public void start(Stage primaryStage) {
selectedCircle.addListener((obs, oldSelection, newSelection) -> {
if (oldSelection != null) {
oldSelection.pseudoClassStateChanged(SELECTED_P_C, false);
}
if (newSelection != null) {
newSelection.pseudoClassStateChanged(SELECTED_P_C, true);
}
});
Pane grid = new Pane();
for (int x = 0 ; x < numColumns; x++) {
double gridX = x*(spacing + radius + radius) + spacing ;
grid.getChildren().add(new Line(gridX, 0, gridX, numRows*(spacing + radius + radius)));
}
for (int y = 0; y < numRows ; y++) {
double gridY = y*(spacing + radius + radius) + spacing ;
grid.getChildren().add(new Line(0, gridY, numColumns*(spacing + radius + radius), gridY));
}
for (int x = 0 ; x < numColumns; x++) {
for (int y = 0 ;y < numRows ; y++) {
grid.getChildren().add(createCircle(x, y));
}
}
Label label = new Label();
label.textProperty().bind(Bindings.createStringBinding(() -> {
Point2D loc = selectedLocation.get();
if (loc == null) {
return "" ;
}
return String.format("Location: [%.0f, %.0f]", loc.getX(), loc.getY());
}, selectedLocation));
BorderPane root = new BorderPane(new ScrollPane(grid));
root.setTop(label);
Scene scene = new Scene(root);
scene.getStylesheets().add("grid.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private Circle createCircle(int x, int y) {
Circle circle = new Circle();
circle.getStyleClass().add("intersection");
circle.setCenterX(x * (spacing + radius + radius) + spacing);
circle.setCenterY(y * (spacing + radius + radius) + spacing);
circle.setRadius(radius);
circle.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
selectedCircle.set(circle);
selectedLocation.set(new Point2D(x, y));
});
return circle ;
}
public static void main(String[] args) {
launch(args);
}
}
with the file grid.css:
.intersection {
-fx-fill: blue ;
}
.intersection:selected {
-fx-fill: gold ;
}
You can get the reference by using getSource of the MouseEvent.
Example in which you can drag a Circle and any other Node:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Circle circle = new Circle( 100,100,50);
circle.setStroke(Color.BLUE);
circle.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.3));
Rectangle rectangle = new Rectangle( 0,0,100,100);
rectangle.relocate(200, 200);
rectangle.setStroke(Color.GREEN);
rectangle.setFill(Color.GREEN.deriveColor(1, 1, 1, 0.3));
Text text = new Text( "Example Text");
text.relocate(300, 300);
Pane root = new Pane();
root.getChildren().addAll(circle, rectangle, text);
MouseGestures mouseGestures = new MouseGestures();
mouseGestures.makeDraggable(circle);
mouseGestures.makeDraggable(rectangle);
mouseGestures.makeDraggable(text);
Scene scene = new Scene(root, 1024, 768);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class MouseGestures {
class DragContext {
double x;
double y;
}
DragContext dragContext = new DragContext();
public void makeDraggable( Node node) {
node.setOnMousePressed( onMousePressedEventHandler);
node.setOnMouseDragged( onMouseDraggedEventHandler);
node.setOnMouseReleased(onMouseReleasedEventHandler);
}
EventHandler<MouseEvent> onMousePressedEventHandler = event -> {
if( event.getSource() instanceof Circle) {
Circle circle = (Circle) (event.getSource());
dragContext.x = circle.getCenterX() - event.getSceneX();
dragContext.y = circle.getCenterY() - event.getSceneY();
} else {
Node node = (Node) (event.getSource());
dragContext.x = node.getTranslateX() - event.getSceneX();
dragContext.y = node.getTranslateY() - event.getSceneY();
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = event -> {
if( event.getSource() instanceof Circle) {
Circle circle = (Circle) (event.getSource());
circle.setCenterX( dragContext.x + event.getSceneX());
circle.setCenterY( dragContext.y + event.getSceneY());
} else {
Node node = (Node) (event.getSource());
node.setTranslateX( dragContext.x + event.getSceneX());
node.setTranslateY( dragContext.y + event.getSceneY());
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = event -> {
};
}
public static void main(String[] args) {
launch(args);
}
}
I have a set of Nodes, Circles, on the stage.
I want to be able to click on one of them and 'select it' (just get a reference to it so I can move it around, change color etc.)
Pane root = new Pane();
root.getChildren().addAll( /* an array of Circle objects */ );
Scene scene = new Scene(root, 500, 500, BACKGROUND_COLOR);
scene.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
// how do I get which Circle I clicked on?
}
});
stage.setTitle(TITLE);
stage.setScene(scene);
stage.show();
I would simply register a listener with each circle itself. Then you already have the reference to the circle with which the listener was registered.
This example pushes the limit a little as to usability, because it has 10,000 circles shown all at once, but it demonstrates the technique:
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class GridOfCircles extends Application {
private static final PseudoClass SELECTED_P_C = PseudoClass.getPseudoClass("selected");
private final int numColumns = 100 ;
private final int numRows = 100 ;
private final double radius = 4 ;
private final double spacing = 2 ;
private final ObjectProperty<Circle> selectedCircle = new SimpleObjectProperty<>();
private final ObjectProperty<Point2D> selectedLocation = new SimpleObjectProperty<>();
#Override
public void start(Stage primaryStage) {
selectedCircle.addListener((obs, oldSelection, newSelection) -> {
if (oldSelection != null) {
oldSelection.pseudoClassStateChanged(SELECTED_P_C, false);
}
if (newSelection != null) {
newSelection.pseudoClassStateChanged(SELECTED_P_C, true);
}
});
Pane grid = new Pane();
for (int x = 0 ; x < numColumns; x++) {
double gridX = x*(spacing + radius + radius) + spacing ;
grid.getChildren().add(new Line(gridX, 0, gridX, numRows*(spacing + radius + radius)));
}
for (int y = 0; y < numRows ; y++) {
double gridY = y*(spacing + radius + radius) + spacing ;
grid.getChildren().add(new Line(0, gridY, numColumns*(spacing + radius + radius), gridY));
}
for (int x = 0 ; x < numColumns; x++) {
for (int y = 0 ;y < numRows ; y++) {
grid.getChildren().add(createCircle(x, y));
}
}
Label label = new Label();
label.textProperty().bind(Bindings.createStringBinding(() -> {
Point2D loc = selectedLocation.get();
if (loc == null) {
return "" ;
}
return String.format("Location: [%.0f, %.0f]", loc.getX(), loc.getY());
}, selectedLocation));
BorderPane root = new BorderPane(new ScrollPane(grid));
root.setTop(label);
Scene scene = new Scene(root);
scene.getStylesheets().add("grid.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private Circle createCircle(int x, int y) {
Circle circle = new Circle();
circle.getStyleClass().add("intersection");
circle.setCenterX(x * (spacing + radius + radius) + spacing);
circle.setCenterY(y * (spacing + radius + radius) + spacing);
circle.setRadius(radius);
circle.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
selectedCircle.set(circle);
selectedLocation.set(new Point2D(x, y));
});
return circle ;
}
public static void main(String[] args) {
launch(args);
}
}
with the file grid.css:
.intersection {
-fx-fill: blue ;
}
.intersection:selected {
-fx-fill: gold ;
}
You can get the reference by using getSource of the MouseEvent.
Example in which you can drag a Circle and any other Node:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Circle circle = new Circle( 100,100,50);
circle.setStroke(Color.BLUE);
circle.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.3));
Rectangle rectangle = new Rectangle( 0,0,100,100);
rectangle.relocate(200, 200);
rectangle.setStroke(Color.GREEN);
rectangle.setFill(Color.GREEN.deriveColor(1, 1, 1, 0.3));
Text text = new Text( "Example Text");
text.relocate(300, 300);
Pane root = new Pane();
root.getChildren().addAll(circle, rectangle, text);
MouseGestures mouseGestures = new MouseGestures();
mouseGestures.makeDraggable(circle);
mouseGestures.makeDraggable(rectangle);
mouseGestures.makeDraggable(text);
Scene scene = new Scene(root, 1024, 768);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class MouseGestures {
class DragContext {
double x;
double y;
}
DragContext dragContext = new DragContext();
public void makeDraggable( Node node) {
node.setOnMousePressed( onMousePressedEventHandler);
node.setOnMouseDragged( onMouseDraggedEventHandler);
node.setOnMouseReleased(onMouseReleasedEventHandler);
}
EventHandler<MouseEvent> onMousePressedEventHandler = event -> {
if( event.getSource() instanceof Circle) {
Circle circle = (Circle) (event.getSource());
dragContext.x = circle.getCenterX() - event.getSceneX();
dragContext.y = circle.getCenterY() - event.getSceneY();
} else {
Node node = (Node) (event.getSource());
dragContext.x = node.getTranslateX() - event.getSceneX();
dragContext.y = node.getTranslateY() - event.getSceneY();
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = event -> {
if( event.getSource() instanceof Circle) {
Circle circle = (Circle) (event.getSource());
circle.setCenterX( dragContext.x + event.getSceneX());
circle.setCenterY( dragContext.y + event.getSceneY());
} else {
Node node = (Node) (event.getSource());
node.setTranslateX( dragContext.x + event.getSceneX());
node.setTranslateY( dragContext.y + event.getSceneY());
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = event -> {
};
}
public static void main(String[] args) {
launch(args);
}
}
I've written a java implementation of Craig Reynolds Boids. I recently updated each object to be represented by a .png image. Ever since I've been having the display issue in the image.
What's the best way to fix the issue?
I've tried using a Polygon but when one of my coordinates is a negative the triangle doesn't display properly.
Main Class:
public void paint(final GraphicsContext g) {
new AnimationTimer() {
#Override
public void handle(long now) {
flock.updateBoidsPostion();
g.clearRect(0, 0, width, height);
flock.drawBoids(g);
}
}.start();
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("Boids Flocking Algorithm");
Group root = new Group();
Canvas canvas = new Canvas(width, height);
GraphicsContext gc = canvas.getGraphicsContext2D();
root.getChildren().add(canvas);
primaryStage.setScene(new Scene(root));
primaryStage.show();
paint(gc);
}
Flock:
/**
* Paint each boid comprising the flock the canvas.
* #param g
*/
public void drawBoids(GraphicsContext g) {
for(Boid aBoid : boids) {
aBoid.draw(g);
}
}
Boid:
public void draw(GraphicsContext g) {
//coordinates for the tip of the boid
int x = (int)this.position.xPos;
int y = (int)this.position.yPos;
//Calculate a angle representing the direction of travel.
Rotate r = new Rotate(angle, x, y);
g.setTransform(r.getMxx(), r.getMyx(), r.getMxy(), r.getMyy(), r.getTx(), r.getTy());
g.drawImage(image, x, y);
}
The problem with your code is that you rotate the GraphicsContext, not the image. Or at least you don't rotate the GraphicsContext back after you rotated it.
I was curious about the link you mentioned, i. e. the Boids Pseudocode.
Here's a quick implementation. Drag the rectangle to have the flock follow it.
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Stage;
// Boids implementation in JavaFX
// Pseudo code by Conrad Parker: http://www.kfish.org/boids/pseudocode.html
public class Main extends Application {
int numBoids = 50;
double boidRadius = 10d;
double boidMinDistance = boidRadius * 2d + 5; // +5 = arbitrary value
double initialBaseVelocity = 1d;
double velocityLimit = 3d;
double movementToCenter = 0.01; // 1% towards center
List<Boid> boids;
static Random rnd = new Random();
double sceneWidth = 1024;
double sceneHeight = 768;
Pane playfield;
Rectangle rectangle;
#Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
playfield = new Pane();
playfield.setPrefSize(sceneWidth, sceneHeight);
Text infoText = new Text( "Drag the rectangle and have the flock follow it");
root.setTop(infoText);
root.setCenter(playfield);
Scene scene = new Scene(root, sceneWidth, sceneHeight, Color.WHITE);
//scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
// create boids
createBoids();
// add boids to scene
playfield.getChildren().addAll(boids);
double w = 20;
double h = 20;
rectangle = new Rectangle( w, h);
rectangle.relocate(sceneWidth / 2 - w/2, sceneHeight / 4 - h/2);
playfield.getChildren().add(rectangle);
MouseGestures mg = new MouseGestures();
mg.makeDraggable(rectangle);
// animation loop
AnimationTimer loop = new AnimationTimer() {
#Override
public void handle(long now) {
boids.forEach(Boid::move);
boids.forEach(Boid::updateUI);
}
};
loop.start();
}
private void createBoids() {
boids = new ArrayList<>();
// margin from top/left/bottom/right, so we have the boids initially more in the center
double marginX = sceneWidth / 4;
double marginY = sceneHeight / 4;
for (int i = 0; i < numBoids; i++) {
// random position around the center
double x = rnd.nextDouble() * (sceneWidth - marginX * 2) + marginX;
double y = rnd.nextDouble() * (sceneHeight - marginY * 2) + marginY;
// initial random velocity depending on speed
double v = Math.random() * 4 + initialBaseVelocity;
Boid boid = new Boid(i, x, y, v);
boids.add(boid);
}
}
// Rule 1: Boids try to fly towards the centre of mass of neighbouring boids.
public Point2D rule1(Boid boid) {
Point2D pcj = new Point2D(0, 0);
for( Boid neighbor: boids) {
if( boid == neighbor)
continue;
pcj = pcj.add( neighbor.position);
}
if( boids.size() > 1) {
double div = 1d / (boids.size() - 1);
pcj = pcj.multiply( div);
}
pcj = (pcj.subtract(boid.position)).multiply( movementToCenter);
return pcj;
}
// Rule 2: Boids try to keep a small distance away from other objects (including other boids).
public Point2D rule2(Boid boid) {
Point2D c = new Point2D(0, 0);
for( Boid neighbor: boids) {
if( boid == neighbor)
continue;
double distance = (neighbor.position.subtract(boid.position)).magnitude();
if( distance < boidMinDistance) {
c = c.subtract(neighbor.position.subtract(boid.position));
}
}
return c;
}
// Rule 3: Boids try to match velocity with near boids.
public Point2D rule3(Boid boid) {
Point2D pvj = new Point2D(0, 0);
for( Boid neighbor: boids) {
if( boid == neighbor)
continue;
pvj = pvj.add( neighbor.velocity);
}
if( boids.size() > 1) {
double div = 1d / (boids.size() - 1);
pvj = pvj.multiply( div);
}
pvj = (pvj.subtract(boid.velocity)).multiply(0.125); // 0.125 = 1/8
return pvj;
}
// tend towards the rectangle
public Point2D tendToPlace( Boid boid) {
Point2D place = new Point2D( rectangle.getBoundsInParent().getMinX() + rectangle.getBoundsInParent().getWidth() / 2, rectangle.getBoundsInParent().getMinY() + rectangle.getBoundsInParent().getHeight() / 2);
return (place.subtract(boid.position)).multiply( 0.01);
}
public class Boid extends Circle {
int id;
Point2D position;
Point2D velocity;
double v;
// random color
Color color = randomColor();
public Boid(int id, double x, double y, double v) {
this.id = id;
this.v = v;
position = new Point2D( x, y);
velocity = new Point2D( v, v);
setRadius( boidRadius);
setStroke(color);
setFill(color.deriveColor(1, 1, 1, 0.2));
}
public void move() {
Point2D v1 = rule1(this);
Point2D v2 = rule2(this);
Point2D v3 = rule3(this);
Point2D v4 = tendToPlace(this);
velocity = velocity
.add(v1)
.add(v2)
.add(v3)
.add(v4)
;
limitVelocity();
position = position.add(velocity);
constrainPosition();
}
private void limitVelocity() {
double vlim = velocityLimit;
if( velocity.magnitude() > vlim) {
velocity = (velocity.multiply(1d/velocity.magnitude())).multiply( vlim);
}
}
// limit position to screen dimensions
public void constrainPosition() {
double xMin = boidRadius;
double xMax = sceneWidth - boidRadius;
double yMin = boidRadius;
double yMax = sceneHeight - boidRadius;
double x = position.getX();
double y = position.getY();
double vx = velocity.getX();
double vy = velocity.getY();
if( x < xMin) {
x = xMin;
vx = v;
}
else if( x > xMax) {
x = xMax;
vx = -v;
}
if( y < yMin) {
y = yMin;
vy = v;
}
else if( y > yMax) {
y = yMax;
vy = -v;
}
// TODO: modification would be less performance consuming => find out how to modify the vector directly or create own Poin2D class
position = new Point2D( x, y);
velocity = new Point2D( vx, vy);
}
public void updateUI() {
setCenterX(position.getX());
setCenterY(position.getY());
}
}
public static Color randomColor() {
int range = 220;
return Color.rgb((int) (rnd.nextDouble() * range), (int) (rnd.nextDouble() * range), (int) (rnd.nextDouble() * range));
}
public static class MouseGestures {
class DragContext {
double x;
double y;
}
DragContext dragContext = new DragContext();
public void makeDraggable( Node node) {
node.setOnMousePressed( onMousePressedEventHandler);
node.setOnMouseDragged( onMouseDraggedEventHandler);
node.setOnMouseReleased( onMouseReleasedEventHandler);
}
EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if( event.getSource() instanceof Circle) {
Circle circle = ((Circle) (event.getSource()));
dragContext.x = circle.getCenterX() - event.getSceneX();
dragContext.y = circle.getCenterY() - event.getSceneY();
} else {
Node node = ((Node) (event.getSource()));
dragContext.x = node.getTranslateX() - event.getSceneX();
dragContext.y = node.getTranslateY() - event.getSceneY();
}
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if( event.getSource() instanceof Circle) {
Circle circle = ((Circle) (event.getSource()));
circle.setCenterX( dragContext.x + event.getSceneX());
circle.setCenterY( dragContext.y + event.getSceneY());
} else {
Node node = ((Node) (event.getSource()));
node.setTranslateX( dragContext.x + event.getSceneX());
node.setTranslateY( dragContext.y + event.getSceneY());
}
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
}
};
}
public static void main(String[] args) {
launch(args);
}
}
A 3D version is just a matter of using Points3D instead of Points2D and Spheres and Boxes instead of Circles and Rectangles.
I also suggest you read the excellent book The Nature of Code by Daniel Shiffman, especially the chapter Autonomous Agents. It deals in detail with the Boids.