In the JavaFX code below outputs two vertex(circles) with a directed edge(line and arrow) from the source to the target. However, the arrow is always the center of the circle. I want the arrow to always point at the edge of the circle when dragged or not.
main class(TestArrow.java):
import java.util.ArrayList;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class TestArrow extends Application
{
int startX = 20,
startY = 20,
endX = 200,
endY = 200;
ArrayList<ArrowSecond> arrows = new ArrayList<>();
#Override
public void start(Stage primaryStage)
{
AnchorPane root = new AnchorPane();
Line line = new Line(startX, startY, endX, endY);
line.setStrokeWidth(3);
line.setStroke(Color.BLACK);
AnchorSecond start = new AnchorSecond(Color.BLACK, line.startXProperty(), line.startYProperty(), this);
AnchorSecond end = new AnchorSecond(Color.BLACK, line.endXProperty(), line.endYProperty(), this);
double[] points = {0.0, 10.0, -10.0, -10.0, 10.0, -10.0};
arrows.add(new ArrowSecond(points, line));
root.getChildren().addAll(arrows);
root.getChildren().addAll(line, start, end);
primaryStage.setScene(new Scene(root, 500, 500));
primaryStage.show();
}
public ArrayList<ArrowSecond> getArrows(){
return arrows;
}
public static void main(String[] args){
launch(args);
}
}
Arrow class(ArrowSecond.java):
import javafx.scene.shape.Line;
import javafx.scene.shape.Polygon;
public class ArrowSecond extends Polygon
{
private Line line;
public ArrowSecond(double[] points, Line line)
{
super(points);
this.line = line;
initialize();
}
private void initialize()
{
double angle = Math.atan2(line.getEndY() - line.getStartY(), line.getEndX() - line.getStartX()) * 180 / 3.14;
setRotate(angle - 90);
setTranslateX(line.getStartX());
setTranslateY(line.getStartY());
setTranslateX(line.getEndX());
setTranslateY(line.getEndY());
}
public void update(){
initialize();
}
}
vertex class(AnchorSecond.java):
import javafx.beans.property.DoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.StrokeType;
public class AnchorSecond extends Circle
{
private double x, y;
TestArrow app;
public AnchorSecond(Color color, DoubleProperty xx, DoubleProperty yy, TestArrow app)
{
// x point y point radius
super(xx.get(), yy.get(), 12);
this.app = app;
setFill(color.deriveColor(1, 1, 1, 0.5));
setStroke(color);
setStrokeWidth(2);
setStrokeType(StrokeType.OUTSIDE);
xx.bind(centerXProperty());
yy.bind(centerYProperty());
setOnMousePressed(new EventHandler<MouseEvent>(){
#Override
public void handle(MouseEvent mouseEvent)
{
x = getCenterX() - mouseEvent.getX();
y = getCenterY() - mouseEvent.getY();
}
});
setOnMouseDragged(new EventHandler<MouseEvent>(){
#Override
public void handle(MouseEvent event)
{
setCenterX(event.getSceneX() - x);
setCenterY(event.getSceneY() - y);
// update arrow positions when circles are dragged
for(ArrowSecond arrow : app.getArrows())
arrow.update();
}
});
}
}
Here is my solution:
I subtracted from the end X and end Y coordinates based on the radius of your nodes. The math is pretty simple - I just subtracted from the hypotenuse as a ratio.
TestArrow class:
public class TestArrow extends Application {
int startX = 20, startY = 20, endX = 200, endY = 200;
ArrayList<ArrowSecond> arrows = new ArrayList<>();
#Override
public void start(Stage primaryStage)
{
double radius = 12;
AnchorPane root = new AnchorPane();
Line line = new Line(startX, startY, endX, endY);
line.setStrokeWidth(3);
line.setStroke(Color.BLACK);
AnchorSecond start = new AnchorSecond(Color.BLACK, line.startXProperty(), line.startYProperty(), radius, this);
AnchorSecond end = new AnchorSecond(Color.BLACK, line.endXProperty(), line.endYProperty(), radius, this);
double[] points = {0.0, 10.0, -10.0, -10.0, 10.0, -10.0};
arrows.add(new ArrowSecond(points, line, radius));
root.getChildren().addAll(arrows);
root.getChildren().addAll(start, end, line);
primaryStage.setScene(new Scene(root, 500, 500));
primaryStage.show();
}
public ArrayList<ArrowSecond> getArrows(){
return arrows;
}
public static void main(String[] args){
launch(args);
}
}
ArrowSecond class:
public class ArrowSecond extends Polygon {
private Line line;
double radius;
public ArrowSecond(double[] points, Line line, double AnchorRadius) {
super(points);
this.line = line;
this.radius = AnchorRadius * 2;
initialize();
}
private void initialize() {
double angle = Math.atan2(line.getEndY() - line.getStartY(), line.getEndX() - line.getStartX()) * 180 / 3.14;
double height = line.getEndY() - line.getStartY();
double width = line.getEndX() - line.getStartX();
double length = Math.sqrt(Math.pow(height, 2) + Math.pow(width, 2));
double subtractWidth = radius * width / length;
double subtractHeight = radius * height / length;
setRotate(angle - 90);
setTranslateX(line.getStartX());
setTranslateY(line.getStartY());
setTranslateX(line.getEndX() - subtractWidth);
setTranslateY(line.getEndY() - subtractHeight);
}
public void update(){
initialize();
}
}
AnchorSecond class:
public class AnchorSecond extends Circle {
private double x, y;
TestArrow app;
public AnchorSecond(Color color, DoubleProperty xx, DoubleProperty yy, double radius, TestArrow app) {
// x point y point radius
super(xx.get(), yy.get(), radius);
this.app = app;
setFill(color.deriveColor(1, 1, 1, 0.5));
setStroke(color);
setStrokeWidth(2);
setStrokeType(StrokeType.OUTSIDE);
xx.bind(centerXProperty());
yy.bind(centerYProperty());
setOnMousePressed(new EventHandler<MouseEvent>(){
#Override
public void handle(MouseEvent mouseEvent)
{
x = getCenterX() - mouseEvent.getX();
y = getCenterY() - mouseEvent.getY();
}
});
setOnMouseDragged(new EventHandler<MouseEvent>(){
#Override
public void handle(MouseEvent event)
{
setCenterX(event.getSceneX() - x);
setCenterY(event.getSceneY() - y);
// update arrow positions when circles are dragged
for(ArrowSecond arrow : app.getArrows())
arrow.update();
}
});
}
}
Sorry for bad formatting- first time answering a SO question
Related
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:
I am trying to build a game myself using JavaFX. It features a circle that must follow my cursor (using a smooth translation animation, so it will not go directly to the location of my cursor)
For now I have written this piece of code
root.setOnMouseMoved(event -> {
TranslateTransition tt = new TranslateTransition(Duration.millis(2000), circle);
x[0] = event.getSceneX();
y[0] = event.getSceneY();
location.setText(x[0] + ", " + y[0]);
if (x[0] != oldX[0] || y[0] != oldY[0]) {
tt.stop();
tt.setToX(event.getSceneX());
tt.setToY(event.getSceneY());
oldX[0] = x[0];
oldY[0] = y[0];
}
tt.play();
});
The location.setText(..) is just a label to see whether the coords are recognized by the program. And in fact they are: for each pixel my cursor moves it updated these numbers instantly.
However, my circle will only go to the location of my cursor when it does not move. I want the shape to follow my cursor on the go as well but it just won't.
So my problem is this: how can I make my circle follow my mouse, even when it is moving?
Use an AnimationTimer for the movement. You may also want to check Mike's Blog about a usage example.
Read The Nature of Code by Daniel Shiffman, especially the Chapter Vectors, 1.10 Interactivity with Acceleration. The webpage has a running example, easily to convert to JavaFX.
Here's the code from the book implemented in JavaFX:
Walker.java
import java.util.Random;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
public class Walker extends Pane {
private static Random random = new Random();
PVector location;
PVector velocity;
PVector acceleration;
float topspeed;
double width = 30;
double height = width;
double centerX = width / 2.0;
double centerY = height / 2.0;
double radius = width / 2.0;
Circle circle;
public Walker() {
location = new PVector(random.nextDouble() * width, random.nextDouble() * height, 0);
velocity = new PVector(0, 0, 0);
topspeed = 4;
circle = new Circle(radius);
circle.setCenterX(radius);
circle.setCenterY(radius);
circle.setStroke(Color.BLUE);
circle.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.3));
getChildren().add(circle);
}
public void step(PVector mouse) {
PVector dir = PVector.sub(mouse, location);
dir.normalize();
dir.mult(0.5);
acceleration = dir;
velocity.add(acceleration);
velocity.limit(topspeed);
location.add(velocity);
}
public void checkBoundaries() {
if (location.x > Settings.SCENE_WIDTH) {
location.x = 0;
} else if (location.x < 0) {
location.x = Settings.SCENE_WIDTH;
}
if (location.y > Settings.SCENE_HEIGHT) {
location.y = 0;
} else if (location.y < 0) {
location.y = Settings.SCENE_HEIGHT;
}
}
public void display() {
relocate(location.x - centerX, location.y - centerY);
}
}
Main.java
import java.util.ArrayList;
import java.util.List;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
Pane playfield;
List<Walker> allWalkers = new ArrayList<>();
PVector mouse = new PVector(0,0,0);
#Override
public void start(Stage primaryStage) {
// create containers
BorderPane root = new BorderPane();
StackPane layerPane = new StackPane();
// playfield for our walkers
playfield = new Pane();
layerPane.getChildren().addAll(playfield);
root.setCenter(layerPane);
Scene scene = new Scene(root, Settings.SCENE_WIDTH, Settings.SCENE_HEIGHT);
primaryStage.setScene(scene);
primaryStage.show();
// add 1 walker
addWalker();
// capture mouse position
scene.addEventFilter(MouseEvent.ANY, e -> {
mouse.set(e.getX(), e.getY(), 0);
});
// process all walkers
AnimationTimer loop = new AnimationTimer() {
#Override
public void handle(long now) {
// move
allWalkers.forEach((walker) -> walker.step(mouse));
// check border
allWalkers.forEach(Walker::checkBoundaries);
// update in fx scene
allWalkers.forEach(Walker::display);
}
};
loop.start();
}
/**
* Add single walker to list of walkers and to the playfield
*/
private void addWalker() {
Walker walker = new Walker();
allWalkers.add(walker);
playfield.getChildren().add(walker);
}
public static void main(String[] args) {
launch(args);
}
}
Settings.java
public class Settings {
public static double SCENE_WIDTH = 800;
public static double SCENE_HEIGHT = 600;
}
PVector.java (you can get the full source from the Processing source code)
public class PVector {
public double x;
public double y;
public double z;
public PVector(double x, double y, double z) {
super();
this.x = x;
this.y = y;
this.z = z;
}
public void normalize() {
double m = mag();
if (m != 0 && m != 1) {
div(m);
}
}
public void div(double value) {
x /= value;
y /= value;
z /= value;
}
public void mult(double value) {
x *= value;
y *= value;
z *= value;
}
public void add(PVector v) {
x += v.x;
y += v.y;
z += v.z;
}
public void sub(PVector v) {
x -= v.x;
y -= v.y;
z -= v.z;
}
public void limit(float max) {
if (mag() > max) {
normalize();
mult(max);
}
}
public double mag() {
return Math.sqrt(x * x + y * y + z * z);
}
public static PVector sub(PVector v1, PVector v2) {
return sub(v1, v2, null);
}
public static PVector sub(PVector v1, PVector v2, PVector target) {
if (target == null) {
target = new PVector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z);
} else {
target.set(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z);
}
return target;
}
public void set(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}
}
So using the tips Roland gave me I have created this piece of code which works just the way I want to be (like I described in my question)
AnimationTimer timer = new AnimationTimer() {
#Override
public void handle(long now) {
TranslateTransition tt = new TranslateTransition(Duration.millis(250), circle);
Point mouse = MouseInfo.getPointerInfo().getLocation();
x[0] = mouse.getX();
y[0] = mouse.getY();
location.setText(x[0] + ", " + y[0]);
tt.setToX(x[0]);
tt.setToY(y[0]);
tt.play();
}
};
timer.start();
The major change is the use of the AnimationTimer instead of an event. This caused me to change the way I retrieve the location of the mouse, now I use the awt.MouseInfo to get the X and Y of my cursor.
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.
is it possible to simply make 360 degree movement in java(swing) without any game engine? all I have is this attempt:
public class Game extends JPanel implements Runnable {
int x = 300;
int y = 500;
float angle = 30;
Game game;
public static void main(String[] args) {
JFrame frame = new JFrame();
frame.add(new Game());
frame.setSize(600, 600);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public Game() {
setSize(600, 600);
Thread thread = new Thread(this);
thread.start();
}
#Override
public void paint(Graphics g) {
g.setColor(Color.WHITE);
g.drawRect(0, 0, 600, 600);
g.setColor(Color.CYAN);
g.fillOval(x, y, 10, 10);
g.dispose();
}
#Override
public void run() {
while(true) {
angle += -0.1;
x += Math.sin(angle);
y--;
repaint();
try {
Thread.sleep(50);
} catch (InterruptedException ex) {}
}
}
}
as you can see in following picture, I don't know how to handle movement rotating, this is the output:
image http://screenshot.cz/GOXE3/mvm.jpg
Actually, this is quite possible.
My preferred way is to actually take advantage of the Graphics transform so that you don't have to do any computation, it's all left to the Graphics
By the way:
since you did not create the Graphics object, don't ever dispose it.
override paintComponent() rather than paint()
It's always a good pattern to call super.paintComponent()
Small demo example:
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.RoundRectangle2D;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class TestRotate {
public static class ShapeAndColor {
private final Shape shape;
private final Color color;
public ShapeAndColor(Shape shape, Color color) {
super();
this.shape = shape;
this.color = color;
}
public Shape getShape() {
return shape;
}
public Color getColor() {
return color;
}
}
public static class RotatingShapesPanel extends JComponent {
private List<ShapeAndColor> shapes;
private double rotation = 0.0;
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
AffineTransform translate = AffineTransform.getTranslateInstance(-getWidth() / 2, -getHeight() / 2);
AffineTransform rotate = AffineTransform.getRotateInstance(rotation);
AffineTransform t = AffineTransform.getTranslateInstance(getWidth() / 2, getHeight() / 2);
t.concatenate(rotate);
t.concatenate(translate);
g2d.setTransform(t);
AffineTransform scale = AffineTransform.getScaleInstance(getWidth(), getHeight());
for (ShapeAndColor shape : shapes) {
Area area = new Area(shape.getShape());
g2d.setColor(shape.getColor());
area.transform(scale);
g2d.fill(area);
}
}
public void setShapes(List<ShapeAndColor> shapes) {
this.shapes = shapes;
repaint();
}
public double getRotation() {
return rotation;
}
public void setRotation(double rotation) {
this.rotation = rotation;
repaint();
}
}
protected void initUI(final boolean useBorderLayout) {
final JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
List<ShapeAndColor> shapes = new ArrayList<>();
Random r = new Random();
for (int i = 0; i < 10; i++) {
double x = r.nextDouble();
double y = r.nextDouble();
double w = r.nextDouble();
double h = r.nextDouble();
w = Math.min(w, 1 - x) / 2;
h = Math.min(h, 1 - y) / 2;
double a = Math.min(w, h) / 10.0;
RoundRectangle2D.Double shape = new RoundRectangle2D.Double(x, y, w, h, a, a);
Color color = new Color(r.nextInt(256), r.nextInt(256), r.nextInt(256));
shapes.add(new ShapeAndColor(shape, color));
}
final RotatingShapesPanel panel = new RotatingShapesPanel();
panel.setShapes(shapes);
frame.add(panel);
frame.setSize(600, 600);
frame.setVisible(true);
Timer t = new Timer(0, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
double rotation = panel.getRotation() + 0.02;
if (rotation > Math.PI * 2) {
rotation -= Math.PI * 2;
}
panel.setRotation(rotation);
}
});
t.setRepeats(true);
t.setDelay(10);
t.start();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new TestRotate().initUI(true);
}
});
}
}
Change a few lines...
int basex = 300; // midpoint of the circle
int basey = 400;
int radius = 100; // radius
int x;
int y;
float angle = 0; // Angles in radians, NOT degrees!
public void run() {
while(true) {
angle += 0.01;
x = (int)(basex + radius*Math.cos(angle));
y = (int)(basey - radius*Math.sin(angle));
repaint();
try {
Thread.sleep(50);
} catch (InterruptedException ex) {}
}
}
Not sure what you were trying to code there, but this is the correct formula for a circular movement.
To calculate the rotation around a point, you need a center point for the rotation (cx, cy), the radius or distance of the point from the center, you need the angle (in radians, not degrees), and you need to use sine and cosine to calculate the offset of the point from the center as it rotates around it.
int cx, cy, radius; // I'll let you determine these
double theta = Math.toRadians(30);
double dtheta = Math.toRadians(-0.1);
double dx = Math.cos(theta) * radius;
double dy = Math.sin(theta) * radius;
int x = (int)(cx + dx);
int y = (int)(cy + dy);
repaint();
theta += dtheta; // step the angle
You program has some problems:
int x = 300;
int y = 500;
You should use a floating point data type like double to store the coordinates. You can cast them to int when you want to draw them. If you store them in int, you'll lose precision.
x += Math.sin(angle);
y--;
This doesn't work, since y is decremented instead of calculated using Math.sin(angle). (us Math.cos for x)
This is your fixed code (unchanged parts are omitted):
double x = 300;
double y = 500;
float angle = 30;
double radius = 10D; // new variable to increase the radius of the drawn circle
Game game;
// main method
// constructor
#Override
public void paint(Graphics g) {
// ... stuff omitted
g.fillOval((int)x, (int)y, 10, 10); // you can cast to int here
g.dispose();
}
#Override
public void run() {
while (true) {
angle -= 0.1; // is the same as `angle += -0.1`
x += radius * Math.cos(angle);
y += radius * Math.sin(angle);
repaint();
// ... try catch block
}
}
This currenlty draw the circle counter-clockwise. If you want to draw it clockwise, then change angle to:
angle += 0.1;
I have to draw an EQ triangle, and the coordinates of two points are already given.
I'm using the drawPolygon method, and so I have to find the third point out.
int xCoord[] = {x1, x2, ?};
int yCoord[] = {y1, y2, ?};
g.drawPolygon(xCoord, yCoord, 3);
Given the two initial points, and the fact that the height of an equilateral triangle with edge length a is a*sqrt(3)/2, you can compute
the edge between the given points
the (normalized) direction of the perpendicular to this edge
the center of this edge
Then the tip points of the equilateral triangle are given as
center +/- height * perpendicularDirection
This is implemented here in the computeTipPoint method:
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class EquilateralTriangleTest
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
createAndShowGUI();
}
});
}
private static void createAndShowGUI()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
EquilateralTrianglePanel panel = new EquilateralTrianglePanel();
f.getContentPane().add(panel);
f.setSize(1000,800);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
class EquilateralTrianglePanel extends JPanel implements MouseMotionListener
{
private final Point2D point0;
private final Point2D point1;
EquilateralTrianglePanel()
{
this.point0 = new Point2D.Double(600,500);
this.point1 = new Point2D.Double(400,600);
addMouseMotionListener(this);
}
#Override
protected void paintComponent(Graphics gr)
{
super.paintComponent(gr);
Graphics2D g = (Graphics2D)gr;
g.setColor(Color.BLACK);
g.draw(new Line2D.Double(point0, point1));
g.setColor(Color.RED);
draw(g, point0);
draw(g, point1);
int x0 = (int)point0.getX();
int y0 = (int)point0.getY();
int x1 = (int)point1.getX();
int y1 = (int)point1.getY();
Point2D point2a = computeTipPoint(point0, point1, true);
int x2a = (int)point2a.getX();
int y2a = (int)point2a.getY();
g.setColor(Color.BLUE);
int xCoorda[] = {x0, x1, x2a};
int yCoorda[] = {y0, y1, y2a};
g.drawPolygon(xCoorda, yCoorda, 3);
Point2D point2b = computeTipPoint(point0, point1, false);
int x2b = (int)point2b.getX();
int y2b = (int)point2b.getY();
g.setColor(Color.MAGENTA);
int xCoordb[] = {x0, x1, x2b};
int yCoordb[] = {y0, y1, y2b};
g.drawPolygon(xCoordb, yCoordb, 3);
}
private static void draw(Graphics2D g, Point2D p)
{
Ellipse2D e = new Ellipse2D.Double(
p.getX()-3, p.getY()-3, 6, 6);
g.fill(e);
}
#Override
public void mouseDragged(MouseEvent e)
{
point0.setLocation(e.getPoint());
repaint();
}
#Override
public void mouseMoved(MouseEvent e)
{
point1.setLocation(e.getPoint());
repaint();
}
private static Point2D computeTipPoint(
Point2D p0, Point2D p1, boolean right)
{
double dx = p1.getX() - p0.getX();
double dy = p1.getY() - p0.getY();
double length = Math.sqrt(dx*dx+dy*dy);
double dirX = dx / length;
double dirY = dy / length;
double height = Math.sqrt(3)/2 * length;
double cx = p0.getX() + dx * 0.5;
double cy = p0.getY() + dy * 0.5;
double pDirX = -dirY;
double pDirY = dirX;
double rx = 0;
double ry = 0;
if (right)
{
rx = cx + height * pDirX;
ry = cy + height * pDirY;
}
else
{
rx = cx - height * pDirX;
ry = cy - height * pDirY;
}
return new Point2D.Double(rx, ry);
}
}
You have to solve following system of equations to find coordinates of the third vertex:
(x-x1)^2 + (y-y1)^2 = R^2
(x-x2)^2 + (y-y2)^2 = R^2
where
R = dist(x1, y1, x2, y2)