I am trying to place a few components inside a ScrollPane. These components should have the ability to be moved across this pane by mouse (click and drag). The ScrollPane itself is pannable and zoomable.
Now if I pick one of them and drag it to a new position the mouse is faster than the component if I have zoomed out. When zoomed in, the component gets moved faster than the mouse movement.
If not zoomed it works until I reach a certain position where the ScrollPane automatically pans.
It must have to do something with the determined coordinates of the nodes. Does anyone have an idea what I have to add to make it work correctly?
My controller class:
public class MainWindowController implements Initializable {
private final double SCALE_DELTA = 1.1;
private final StackPane zoomPane = new StackPane();
private Group group = new Group();
#FXML
private ScrollPane scrollPane;
#Override
public void initialize(URL url, ResourceBundle rb) {
Node node1 = new Node("Test");
Node node2 = new Node("Test2", 100, 200);
group.getChildren().addAll(node1, node2);
zoomPane.getChildren().add(group);
Group scrollContent = new Group(zoomPane);
scrollPane.setContent(scrollContent);
scrollPane.viewportBoundsProperty().addListener((ObservableValue<? extends Bounds> observable,
Bounds oldValue, Bounds newValue) -> {
zoomPane.setMinSize(newValue.getWidth(), newValue.getHeight());
});
zoomPane.setOnScroll(
(ScrollEvent event) -> {
event.consume();
if (event.getDeltaY() == 0) {
return;
}
double scaleFactor = (event.getDeltaY() > 0) ? SCALE_DELTA : 1 / SCALE_DELTA;
Point2D scrollOffset = figureScrollOffset(scrollContent, scrollPane);
group.setScaleX(group.getScaleX() * scaleFactor);
group.setScaleY(group.getScaleY() * scaleFactor);
repositionScroller(scrollContent, scrollPane, scaleFactor, scrollOffset);
}
);
group.getChildren()
.add(new Node("Test3", 500, 500));
}
private Point2D figureScrollOffset(javafx.scene.Node scrollContent, ScrollPane scroller) {
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
double hScrollProportion = (scroller.getHvalue() - scroller.getHmin()) / (scroller.getHmax() - scroller.getHmin());
double scrollXOffset = hScrollProportion * Math.max(0, extraWidth);
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
double vScrollProportion = (scroller.getVvalue() - scroller.getVmin()) / (scroller.getVmax() - scroller.getVmin());
double scrollYOffset = vScrollProportion * Math.max(0, extraHeight);
return new Point2D(scrollXOffset, scrollYOffset);
}
private void repositionScroller(javafx.scene.Node scrollContent, ScrollPane scroller, double scaleFactor, Point2D scrollOffset) {
double scrollXOffset = scrollOffset.getX();
double scrollYOffset = scrollOffset.getY();
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
if (extraWidth > 0) {
double halfWidth = scroller.getViewportBounds().getWidth() / 2;
double newScrollXOffset = (scaleFactor - 1) * halfWidth + scaleFactor * scrollXOffset;
scroller.setHvalue(scroller.getHmin() + newScrollXOffset * (scroller.getHmax() - scroller.getHmin()) / extraWidth);
} else {
scroller.setHvalue(scroller.getHmin());
}
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
if (extraHeight > 0) {
double halfHeight = scroller.getViewportBounds().getHeight() / 2;
double newScrollYOffset = (scaleFactor - 1) * halfHeight + scaleFactor * scrollYOffset;
scroller.setVvalue(scroller.getVmin() + newScrollYOffset * (scroller.getVmax() - scroller.getVmin()) / extraHeight);
} else {
scroller.setHvalue(scroller.getHmin());
}
}
}
The node class:
public class Node extends Parent {
private NodeStatus status = NodeStatus.OK;
private final Image okImage = new Image(getClass().getResourceAsStream("/images/MasterOK.png"));
private ImageView image = new ImageView(okImage);
private final Text label = new Text();
private final Font font = Font.font("Courier", 20);
double orgSceneX, orgSceneY;
double layoutX, layoutY;
public Node(String labelText) {
this(labelText, 0, 0);
}
public Node(String labelText, double x, double y) {
label.setText(labelText);
label.setFont(font);
label.setLayoutX(okImage.getWidth() + 10);
label.setLayoutY(okImage.getHeight() / 2 + 10);
getChildren().add(image);
getChildren().add(label);
setLayoutX(x);
setLayoutY(y);
setCursor(Cursor.MOVE);
setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
orgSceneX = t.getSceneX();
orgSceneY = t.getSceneY();
layoutX = getLayoutX();
layoutY = getLayoutY();
}
});
setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
setLayoutX(layoutX + t.getSceneX() - orgSceneX);
setLayoutY(layoutY + t.getSceneY() - orgSceneY);
}
});
}
public NodeStatus getStatus() {
return status;
}
public void setStatus(NodeStatus status) {
this.status = status;
}
}
class Delta {
double x, y;
}
and the fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import nodes.*?>
<AnchorPane id="AnchorPane" prefHeight="600.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="cqsmonitor.MainWindowController">
<children>
<Pane layoutX="666.0" layoutY="14.0" prefHeight="572.0" prefWidth="114.0" AnchorPane.bottomAnchor="14.0" AnchorPane.rightAnchor="14.0" AnchorPane.topAnchor="14.0">
<children>
<TextField layoutY="30.0" prefHeight="25.0" prefWidth="114.0" />
<Label layoutY="12.0" text="Search:" />
<ChoiceBox layoutY="90.0" prefHeight="25.0" prefWidth="114.0" />
<Label layoutY="73.0" text="View:" />
</children>
</Pane>
<ScrollPane fx:id="scrollPane" layoutX="14.0" layoutY="14.0" pannable="true" prefHeight="571.0" prefWidth="644.0" AnchorPane.bottomAnchor="15.0" AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="142.0" AnchorPane.topAnchor="14.0">
</ScrollPane>
</children>
</AnchorPane>
Since nobody answered yet, here's some code. I don't want to dig into yours and re-invent the wheel.
You can move nodes by dragging with the left mouse button, scale the pane with the mouse wheel, pan the pane with the right mouse button. No ScrollPane needed. However, if you want ScrollBars, you can always add them if you prefer them.
The code:
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
/**
* The canvas which holds all of the nodes of the application.
*/
class PannableCanvas extends Pane {
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public PannableCanvas() {
setPrefSize(600, 600);
setStyle("-fx-background-color: lightgrey; -fx-border-color: blue;");
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
// logging
addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
System.out.println(
"canvas event: " + ( ((event.getSceneX() - getBoundsInParent().getMinX()) / getScale()) + ", scale: " + getScale())
);
System.out.println( "canvas bounds: " + getBoundsInParent());
});
}
/**
* Add a grid to the canvas, send it to back
*/
public void addGrid() {
double w = getBoundsInLocal().getWidth();
double h = getBoundsInLocal().getHeight();
// add grid
Canvas grid = new Canvas(w, h);
// don't catch mouse events
grid.setMouseTransparent(true);
GraphicsContext gc = grid.getGraphicsContext2D();
gc.setStroke(Color.GRAY);
gc.setLineWidth(1);
// draw grid lines
double offset = 50;
for( double i=offset; i < w; i+=offset) {
// vertical
gc.strokeLine( i, 0, i, h);
// horizontal
gc.strokeLine( 0, i, w, i);
}
getChildren().add( grid);
grid.toBack();
}
public double getScale() {
return myScale.get();
}
/**
* Set x/y scale
* #param myScale
*/
public void setScale( double scale) {
myScale.set(scale);
}
/**
* Set x/y pivot points
* #param x
* #param y
*/
public void setPivot( double x, double y) {
setTranslateX(getTranslateX()-x);
setTranslateY(getTranslateY()-y);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the nodes draggable via left mouse button. Considers if parent is zoomed.
*/
class NodeGestures {
private DragContext nodeDragContext = new DragContext();
PannableCanvas canvas;
public NodeGestures( PannableCanvas canvas) {
this.canvas = canvas;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// left mouse button => dragging
if( !event.isPrimaryButtonDown())
return;
nodeDragContext.mouseAnchorX = event.getSceneX();
nodeDragContext.mouseAnchorY = event.getSceneY();
Node node = (Node) event.getSource();
nodeDragContext.translateAnchorX = node.getTranslateX();
nodeDragContext.translateAnchorY = node.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// left mouse button => dragging
if( !event.isPrimaryButtonDown())
return;
double scale = canvas.getScale();
Node node = (Node) event.getSource();
node.setTranslateX(nodeDragContext.translateAnchorX + (( event.getSceneX() - nodeDragContext.mouseAnchorX) / scale));
node.setTranslateY(nodeDragContext.translateAnchorY + (( event.getSceneY() - nodeDragContext.mouseAnchorY) / scale));
event.consume();
}
};
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
class SceneGestures {
private static final double MAX_SCALE = 10.0d;
private static final double MIN_SCALE = .1d;
private DragContext sceneDragContext = new DragContext();
PannableCanvas canvas;
public SceneGestures( PannableCanvas canvas) {
this.canvas = canvas;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
public EventHandler<ScrollEvent> getOnScrollEventHandler() {
return onScrollEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
sceneDragContext.mouseAnchorX = event.getSceneX();
sceneDragContext.mouseAnchorY = event.getSceneY();
sceneDragContext.translateAnchorX = canvas.getTranslateX();
sceneDragContext.translateAnchorY = canvas.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
canvas.setTranslateX(sceneDragContext.translateAnchorX + event.getSceneX() - sceneDragContext.mouseAnchorX);
canvas.setTranslateY(sceneDragContext.translateAnchorY + event.getSceneY() - sceneDragContext.mouseAnchorY);
event.consume();
}
};
/**
* Mouse wheel handler: zoom to pivot point
*/
private EventHandler<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double delta = 1.2;
double scale = canvas.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
if (event.getDeltaY() < 0)
scale /= delta;
else
scale *= delta;
scale = clamp( scale, MIN_SCALE, MAX_SCALE);
double f = (scale / oldScale)-1;
double dx = (event.getSceneX() - (canvas.getBoundsInParent().getWidth()/2 + canvas.getBoundsInParent().getMinX()));
double dy = (event.getSceneY() - (canvas.getBoundsInParent().getHeight()/2 + canvas.getBoundsInParent().getMinY()));
canvas.setScale( scale);
// note: pivot value must be untransformed, i. e. without scaling
canvas.setPivot(f*dx, f*dy);
event.consume();
}
};
public static double clamp( double value, double min, double max) {
if( Double.compare(value, min) < 0)
return min;
if( Double.compare(value, max) > 0)
return max;
return value;
}
}
/**
* An application with a zoomable and pannable canvas.
*/
public class ZoomAndScrollApplication extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Group group = new Group();
// create canvas
PannableCanvas canvas = new PannableCanvas();
// we don't want the canvas on the top/left in this example => just
// translate it a bit
canvas.setTranslateX(100);
canvas.setTranslateY(100);
// create sample nodes which can be dragged
NodeGestures nodeGestures = new NodeGestures( canvas);
Label label1 = new Label("Draggable node 1");
label1.setTranslateX(10);
label1.setTranslateY(10);
label1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Label label2 = new Label("Draggable node 2");
label2.setTranslateX(100);
label2.setTranslateY(100);
label2.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label2.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Label label3 = new Label("Draggable node 3");
label3.setTranslateX(200);
label3.setTranslateY(200);
label3.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label3.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Circle circle1 = new Circle( 300, 300, 50);
circle1.setStroke(Color.ORANGE);
circle1.setFill(Color.ORANGE.deriveColor(1, 1, 1, 0.5));
circle1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
circle1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Rectangle rect1 = new Rectangle(100,100);
rect1.setTranslateX(450);
rect1.setTranslateY(450);
rect1.setStroke(Color.BLUE);
rect1.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.5));
rect1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
rect1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
canvas.getChildren().addAll(label1, label2, label3, circle1, rect1);
group.getChildren().add(canvas);
// create scene which can be dragged and zoomed
Scene scene = new Scene(group, 1024, 768);
SceneGestures sceneGestures = new SceneGestures(canvas);
scene.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scene.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scene.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
stage.setScene(scene);
stage.show();
canvas.addGrid();
}
}
Let me know if this doesn't help you at all, then I'll delete the post. I'm not interested in the bounty.
I'll just post an answer to summarize the solution to my problem:
The problem why my nodes where lagging behind was because the had no clue about the scaling that might have occured in the pane one level above. So as a final solution I mixed my code with Roland's answer. if anyone has further optimizations in mind, please post them.
Controller class:
public class MainWindowController implements Initializable {
private final Group group = new Group();
private static final double MAX_SCALE = 10.0d;
private static final double MIN_SCALE = .1d;
#FXML
private ScrollPane scrollPane;
#Override
public void initialize(URL url, ResourceBundle rb) {
ZoomableCanvas canvas = new ZoomableCanvas();
MasterNode node1 = new MasterNode("Test");
MasterNode node2 = new MasterNode("Test", 100, 200);
canvas.getChildren().add(node1);
canvas.getChildren().add(node2);
group.getChildren().add(canvas);
scrollPane.setContent(group);
scrollPane.addEventHandler(ScrollEvent.ANY, new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double delta = 1.2;
double scale = canvas.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
if (event.getDeltaY() < 0) {
scale /= delta;
} else {
scale *= delta;
}
scale = clamp(scale, MIN_SCALE, MAX_SCALE);
double f = (scale / oldScale) - 1;
double dx = (event.getSceneX() - (canvas.getBoundsInParent().getWidth() / 2 + canvas.getBoundsInParent().getMinX()));
double dy = (event.getSceneY() - (canvas.getBoundsInParent().getHeight() / 2 + canvas.getBoundsInParent().getMinY()));
canvas.setScale(scale);
// note: pivot value must be untransformed, i. e. without scaling
canvas.setPivot(f * dx, f * dy);
event.consume();
}
});
}
private double clamp(double value, double min, double max) {
if (Double.compare(value, min) < 0) {
return min;
}
if (Double.compare(value, max) > 0) {
return max;
}
return value;
}
}
The canvas:
public class ZoomableCanvas extends Pane {
DoubleProperty scale = new SimpleDoubleProperty(1.0);
public ZoomableCanvas() {
scaleXProperty().bind(scale);
scaleYProperty().bind(scale);
getChildren().addListener((Change<? extends javafx.scene.Node> c) -> {
while (c.next()) {
if (c.wasAdded()) {
for (Node child : c.getAddedSubList()) {
((MasterNode) child).scaleProperty.bind(scale);
}
}
if (c.wasRemoved()) {
for (Node child : c.getRemoved()) {
((MasterNode) child).scaleProperty.unbind();
}
}
}
});
}
public double getScale() {
return scale.get();
}
public void setScale(double scale) {
this.scale.set(scale);
}
public void setPivot(double x, double y) {
setTranslateX(getTranslateX() - x);
setTranslateY(getTranslateY() - y);
}
}
Node:
public class MasterNode extends Parent {
private NodeStatus status = NodeStatus.OK;
private final Image okImage = new Image(getClass().getResourceAsStream("/images/MasterOK.png"));
private ImageView image = new ImageView(okImage);
private final Text label = new Text();
private final Font font = Font.font("Courier", 20);
private DragContext nodeDragContext = new DragContext();
public DoubleProperty scaleProperty = new SimpleDoubleProperty(1.0);
double orgSceneX, orgSceneY;
double layoutX, layoutY;
public MasterNode(String labelText) {
this(labelText, 0, 0);
}
public MasterNode(String labelText, double x, double y) {
scaleXProperty().bind(scaleProperty);
scaleYProperty().bind(scaleProperty);
label.setText(labelText);
label.setFont(font);
label.setLayoutX(okImage.getWidth() + 10);
label.setLayoutY(okImage.getHeight() / 2 + 10);
getChildren().add(image);
getChildren().add(label);
setLayoutX(x);
setLayoutY(y);
setCursor(Cursor.MOVE);
setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
// left mouse button => dragging
if (!event.isPrimaryButtonDown()) {
return;
}
nodeDragContext.setMouseAnchorX(event.getSceneX());
nodeDragContext.setMouseAnchorY(event.getSceneY());
Node node = (Node) event.getSource();
nodeDragContext.setTranslateAnchorX(node.getTranslateX());
nodeDragContext.setTranslateAnchorY(node.getTranslateY());
}
});
setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
// left mouse button => dragging
if (!event.isPrimaryButtonDown()) {
return;
}
Node node = (Node) event.getSource();
node.setTranslateX(nodeDragContext.getTranslateAnchorX() + ((event.getSceneX() - nodeDragContext.getMouseAnchorX()) / getScale()));
node.setTranslateY(nodeDragContext.getTranslateAnchorY() + ((event.getSceneY() - nodeDragContext.getMouseAnchorY()) / getScale()));
event.consume();
}
});
}
public double getScale() {
return scaleProperty.get();
}
public void setScale(double scale) {
scaleProperty.set(scale);
}
public NodeStatus getStatus() {
return status;
}
public void setStatus(NodeStatus status) {
this.status = status;
}
}
Related
So basically i am making a game in which the circle will move in the four directions. When the "S" or "RIGHT" key is pressed once, the circle will move till it reaches the maxX of the pane. And when i press the "A" key or "LEFT", the circle will move till it reaches minX of pane. The "UP" or "W" and "DOWN" or "Z" keys also follow similar logic. If it get to maxY of pane it stops till another key is pressed and vice versa.
Here is the code for the controller class
#FXML
private Pane board;
private BooleanProperty wPressed = new SimpleBooleanProperty();
private BooleanProperty aPressed = new SimpleBooleanProperty();
private BooleanProperty zPressed = new SimpleBooleanProperty();
private BooleanProperty sPressed = new SimpleBooleanProperty();
private BooleanBinding keyPressed = wPressed.or(aPressed).or(zPressed).or(sPressed);
private Circle circle = new Circle(20,40,20);
private Rectangle[][] grid;
private int size = 960;
private int spots = 16;
private int squareSize = size / spots;
#FXML
void handleB(ActionEvent event) {
}
AnimationTimer timer = new AnimationTimer() {
double deltaX = 2;
double deltaY = 2;
#Override
public void handle(long timestamp) {
double rectX = circle.getLayoutX();
Bounds bounds = board.getBoundsInLocal();
double maxX = bounds.getMaxX();
double maxY = bounds.getMaxY();
double minX = bounds.getMinX();
double minY = bounds.getMinY();
if (circle.getLayoutX() >= ( maxX- circle.getRadius())) {
deltaX = 0;
//System.out.println(rect.getLayoutX());
}
if (circle.getLayoutY() >= (maxY - circle.getRadius())) {
deltaY = 0;
}
if (circle.getLayoutX() <= ( minX + circle.getRadius())) {
deltaX = 0;
//System.out.println(rect.getLayoutX());
}
if (circle.getLayoutY() <= ( minY + circle.getRadius())) {
deltaY = 0;
}
if(wPressed.get()) {
circle.setLayoutY(circle.getLayoutY() - deltaY);
}
if(zPressed.get()){
circle.setLayoutY(circle.getLayoutY() + deltaY);
}
if(aPressed.get()){
circle.setLayoutX(circle.getLayoutX() - deltaX);
}
if(sPressed.get()){
circle.setLayoutX(circle.getLayoutX() + deltaX);
}
}
};
#Override
public void initialize(URL url, ResourceBundle resourceBundle) {
grid = new Rectangle[spots][spots];
for (int i = 0; i < size; i += squareSize) {
for (int j = 0; j < size; j += squareSize) {
Rectangle r = new Rectangle(i, j, squareSize, squareSize);
grid[i / squareSize][j / squareSize] = r;
// Filling each grid with the cell image
r.setFill(Color.GRAY);
r.setStroke(Color.BLACK);
board.getChildren().add(r);
}
}
board.getChildren().addAll(circle);
movementSetup();
keyPressed.addListener(((observableValue, aBoolean, t1) -> {
if(!aBoolean){
timer.start();
} else {
timer.stop();
}
}));
}
private void movementSetup() {
board.setOnKeyPressed(e -> {
if(e.getCode() == KeyCode.UP || e.getCode() == KeyCode.W ) {
zPressed.set(false);
sPressed.set(false);
aPressed.set(false);
wPressed.set(true);
}
if(e.getCode() == KeyCode.A || e.getCode() == KeyCode.LEFT) {
zPressed.set(false);
sPressed.set(false);
wPressed.set(false);
aPressed.set(true);
}
if(e.getCode() == KeyCode.Z || e.getCode() == KeyCode.DOWN) {
wPressed.set(false);
aPressed.set(false);
sPressed.set(false);
zPressed.set(true);
}
if(e.getCode() == KeyCode.S || e.getCode() == KeyCode.RIGHT) {
wPressed.set(false);
aPressed.set(false);
zPressed.set(false);
sPressed.set(true);
}
});
}
}
** I did the pane in scenebuilder which i inserted it into an anchorpane as well**
The fxml file is below (ui.fxml)
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.Pane?>
<AnchorPane maxHeight="1000.0" maxWidth="1200.0" minHeight="0.0" minWidth="0.0" prefHeight="1000.0" prefWidth="1200.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="fx.game.Controller">
<children>
<Pane fx:id="board" maxHeight="960.0" maxWidth="960.0" minHeight="0.0" minWidth="0.0" prefHeight="960.0" prefWidth="960.0">
<children>
<Button layoutX="367.0" layoutY="934.0" mnemonicParsing="false" onAction="#handleB" text="Button" />
</children>
</Pane>
</children>
</AnchorPane>
Lastly the main class
package fx.game;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class HelloApplication extends Application {
#Override
public void start(Stage stage) throws IOException {
Parent root = FXMLLoader.load(getClass().getResource("ui.fxml"));
stage.setTitle("Hello World");
stage.setScene(new Scene(root));
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Am really bad at game development
I might be doing it completely wrong
Anyone who can give me a better solution will be appreciated
From my limited understanding and testing, board.setOnKeyPressed isn't responding to key events, I used the Scene instead. You should also be taking into account onKeyReleased.
I don't know why you're starting and stoping the timer, just start it and on each, evaluate the state and make the changes that are required.
Your movement logic seems to be off as well. Get the circles current location, apply any movement to it as required, then determine if the new position is outside the acceptable bounds, adjust the position as needed and then update the circles position.
I also found that the Bounds the board would update based on the position of it's children, which made it hard to do bounds checking on, instead, I calculate the expected size based on the squareSize and the number of spots
For example...
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Bounds;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class App extends Application {
private BooleanProperty wPressed = new SimpleBooleanProperty();
private BooleanProperty aPressed = new SimpleBooleanProperty();
private BooleanProperty zPressed = new SimpleBooleanProperty();
private BooleanProperty sPressed = new SimpleBooleanProperty();
private BooleanBinding keyPressed = wPressed.or(aPressed).or(zPressed).or(sPressed);
private Circle circle = new Circle(20, 20, 20);
private Rectangle[][] grid;
private int size = 960;
private int spots = 16;
private int squareSize = size / spots;
AnimationTimer timer = new AnimationTimer() {
double deltaX = 2;
double deltaY = 2;
#Override
public void handle(long timestamp) {
double rectX = circle.getLayoutX();
Bounds bounds = board.getBoundsInLocal();
double width = squareSize * spots;
double height = squareSize * spots;
double diameter = circle.getRadius() * 2;
double x = circle.getLayoutX();
double y = circle.getLayoutY();
if (wPressed.get()) {
y -= deltaY;
}
if (zPressed.get()) {
y += deltaY;
}
if (aPressed.get()) {
x -= deltaX;
}
if (sPressed.get()) {
x += deltaX;
}
if (x >= (width - diameter)) {
x = width - diameter;
}
if (x < 0) {
x = 0;
}
if (y > (height - diameter)) {
y = height - diameter;
}
if (y < 0) {
y = 0;
}
circle.setLayoutX(x);
circle.setLayoutY(y);
}
};
private Pane board = new AnchorPane();
#Override
public void start(Stage stage) {
stage.setTitle("Dice Game");
setup();
Scene scene = new Scene(board);
movementSetup(scene);
stage.setScene(scene);
stage.sizeToScene();
stage.show();
}
private void movementSetup(Scene scene) {
scene.setOnKeyPressed(e -> {
zPressed.set(false);
sPressed.set(false);
aPressed.set(false);
wPressed.set(false);
if (e.getCode() == KeyCode.UP || e.getCode() == KeyCode.W) {
wPressed.set(true);
} else if (e.getCode() == KeyCode.A || e.getCode() == KeyCode.LEFT) {
aPressed.set(true);
}else if (e.getCode() == KeyCode.Z || e.getCode() == KeyCode.DOWN) {
zPressed.set(true);
}else if (e.getCode() == KeyCode.S || e.getCode() == KeyCode.RIGHT) {
sPressed.set(true);
}
});
}
protected void setup() {
grid = new Rectangle[spots][spots];
for (int i = 0; i < size; i += squareSize) {
for (int j = 0; j < size; j += squareSize) {
Rectangle r = new Rectangle(i, j, squareSize, squareSize);
grid[i / squareSize][j / squareSize] = r;
// Filling each grid with the cell image
r.setFill(Color.GRAY);
r.setStroke(Color.BLACK);
board.getChildren().add(r);
}
}
board.getChildren().addAll(circle);
timer.start();
}
public static void main(String[] args) {
launch();
}
}
nb: I'm a complete JavaFX noob, so there's probably better ways to approach this issue and maybe having a look at things like Introduction to JavaFX for Game Development (or other "javafx game development blogs" might provide better ideas
Currently experimenting with ScalaFX a bit.
Imagine the following:
I have some nodes and they are connected by some edges.
Now when I click the mousebutton I want to select the ones next to the mouse click, e.g. if I click between 1 and 2, I want those two to be selected, if I click before 0, only that one (as it's the first) etc.
Currently (and just as a proof of concept) I am doing this by adding in some helper structures. I have a HashMap of type [Index, Node] and select them like so:
wrapper.onMouseClicked = (mouseEvent: MouseEvent) =>
{
val lowerIndex: Int = (mouseEvent.sceneX).toString.charAt(0).asDigit
val left = nodes.get(lowerIndex)
val right = nodes.get(lowerIndex+1)
left.get.look.setStyle("-fx-background-color: orange;")
right.get.look.setStyle("-fx-background-color: orange;")
}
this does it's just, but I need to have an additional datastructure and it will get really tedious in 2D, like when I have a Y coordinate as well.
What I would prefer would be some method like mentioned in
How to detect Node at specific point in JavaFX?
or
JavaFX 2.2 get node at coordinates (visual tree hit testing)
These questions are based on older versions of JavaFX and use deprecated methods.
I could not find any replacement or solution in ScalaFX 8 so far. Is there a nice way to get all the nodes within a certain radius?
So "Nearest neighbor search" is the general problem you are trying to solve.
Your problem statement is a bit short on details. E.g., are nodes equidistant from each other? are nodes arranged in a grid pattern or randomly? is the node distance modeled based upon a point at the node center, a surrounding box, the actual closest point on an arbitrarily shaped node? etc.
I'll assume randomly placed shapes that may overlap, and picking is not based upon painting order, but on the closest corners of the bounding boxes of shapes. A more accurate picker might work by comparing the clicked point against against an elliptical area surrounding the actual shape rather than the shapes bounding box (as the current picker will be a bit finicky to use for things like overlapping diagonal lines).
A k-d tree algorithm or an R-tree could be used, but in general a linear brute force search will probably just work fine for most applications.
Sample brute force solution algorithm
private Node findNearestNode(ObservableList<Node> nodes, double x, double y) {
Point2D pClick = new Point2D(x, y);
Node nearestNode = null;
double closestDistance = Double.POSITIVE_INFINITY;
for (Node node : nodes) {
Bounds bounds = node.getBoundsInParent();
Point2D[] corners = new Point2D[] {
new Point2D(bounds.getMinX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMaxY()),
new Point2D(bounds.getMinX(), bounds.getMaxY()),
};
for (Point2D pCompare: corners) {
double nextDist = pClick.distance(pCompare);
if (nextDist < closestDistance) {
closestDistance = nextDist;
nearestNode = node;
}
}
}
return nearestNode;
}
Executable Solution
import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.geometry.*;
import javafx.scene.*;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import java.io.IOException;
import java.net.*;
import java.util.Random;
public class FindNearest extends Application {
private static final int N_SHAPES = 10;
private static final double W = 600, H = 400;
private ShapeMachine machine;
public static void main(String[] args) {
launch(args);
}
#Override
public void init() throws MalformedURLException, URISyntaxException {
double maxShapeSize = W / 8;
double minShapeSize = maxShapeSize / 2;
machine = new ShapeMachine(W, H, maxShapeSize, minShapeSize);
}
#Override
public void start(final Stage stage) throws IOException, URISyntaxException {
Pane pane = new Pane();
pane.setPrefSize(W, H);
for (int i = 0; i < N_SHAPES; i++) {
pane.getChildren().add(machine.randomShape());
}
pane.setOnMouseClicked(event -> {
Node node = findNearestNode(pane.getChildren(), event.getX(), event.getY());
highlightSelected(node, pane.getChildren());
});
Scene scene = new Scene(pane);
configureExitOnAnyKey(stage, scene);
stage.setScene(scene);
stage.setResizable(false);
stage.show();
}
private void highlightSelected(Node selected, ObservableList<Node> children) {
for (Node node: children) {
node.setEffect(null);
}
if (selected != null) {
selected.setEffect(new DropShadow(10, Color.YELLOW));
}
}
private Node findNearestNode(ObservableList<Node> nodes, double x, double y) {
Point2D pClick = new Point2D(x, y);
Node nearestNode = null;
double closestDistance = Double.POSITIVE_INFINITY;
for (Node node : nodes) {
Bounds bounds = node.getBoundsInParent();
Point2D[] corners = new Point2D[] {
new Point2D(bounds.getMinX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMaxY()),
new Point2D(bounds.getMinX(), bounds.getMaxY()),
};
for (Point2D pCompare: corners) {
double nextDist = pClick.distance(pCompare);
if (nextDist < closestDistance) {
closestDistance = nextDist;
nearestNode = node;
}
}
}
return nearestNode;
}
private void configureExitOnAnyKey(final Stage stage, Scene scene) {
scene.setOnKeyPressed(keyEvent -> stage.hide());
}
}
Auxiliary random shape generation class
This class is not key to the solution, it just generates some shapes for testing.
class ShapeMachine {
private static final Random random = new Random();
private final double canvasWidth, canvasHeight, maxShapeSize, minShapeSize;
ShapeMachine(double canvasWidth, double canvasHeight, double maxShapeSize, double minShapeSize) {
this.canvasWidth = canvasWidth;
this.canvasHeight = canvasHeight;
this.maxShapeSize = maxShapeSize;
this.minShapeSize = minShapeSize;
}
private Color randomColor() {
return Color.rgb(random.nextInt(256), random.nextInt(256), random.nextInt(256), 0.1 + random.nextDouble() * 0.9);
}
enum Shapes {Circle, Rectangle, Line}
public Shape randomShape() {
Shape shape = null;
switch (Shapes.values()[random.nextInt(Shapes.values().length)]) {
case Circle:
shape = randomCircle();
break;
case Rectangle:
shape = randomRectangle();
break;
case Line:
shape = randomLine();
break;
default:
System.out.println("Unknown Shape");
System.exit(1);
}
Color fill = randomColor();
shape.setFill(fill);
shape.setStroke(deriveStroke(fill));
shape.setStrokeWidth(deriveStrokeWidth(shape));
shape.setStrokeLineCap(StrokeLineCap.ROUND);
shape.relocate(randomShapeX(), randomShapeY());
return shape;
}
private double deriveStrokeWidth(Shape shape) {
return Math.max(shape.getLayoutBounds().getWidth() / 10, shape.getLayoutBounds().getHeight() / 10);
}
private Color deriveStroke(Color fill) {
return fill.desaturate();
}
private double randomShapeSize() {
double range = maxShapeSize - minShapeSize;
return random.nextDouble() * range + minShapeSize;
}
private double randomShapeX() {
return random.nextDouble() * (canvasWidth + maxShapeSize) - maxShapeSize / 2;
}
private double randomShapeY() {
return random.nextDouble() * (canvasHeight + maxShapeSize) - maxShapeSize / 2;
}
private Shape randomLine() {
int xZero = random.nextBoolean() ? 1 : 0;
int yZero = random.nextBoolean() || xZero == 0 ? 1 : 0;
int xSign = random.nextBoolean() ? 1 : -1;
int ySign = random.nextBoolean() ? 1 : -1;
return new Line(0, 0, xZero * xSign * randomShapeSize(), yZero * ySign * randomShapeSize());
}
private Shape randomRectangle() {
return new Rectangle(0, 0, randomShapeSize(), randomShapeSize());
}
private Shape randomCircle() {
double radius = randomShapeSize() / 2;
return new Circle(radius, radius, radius);
}
}
Further example placing objects in a zoomable/scrollable area
This solution uses the nearest node solution code from above and combines it with the zoomed node in a ScrollPane code from: JavaFX correct scaling. The purpose is to demonstrate that the choosing algorithm works even on nodes which have had a scaling transform applied to them (because it is based upon boundsInParent). The code is just meant as a proof of concept and not as a stylistic sample of how to structure the functionality into a class domain model :-)
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.*;
import javafx.collections.ObservableList;
import javafx.event.*;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
public class GraphicsScalingApp extends Application {
private static final int N_SHAPES = 10;
private static final double W = 600, H = 400;
private ShapeMachine machine;
public static void main(String[] args) {
launch(args);
}
#Override
public void init() throws MalformedURLException, URISyntaxException {
double maxShapeSize = W / 8;
double minShapeSize = maxShapeSize / 2;
machine = new ShapeMachine(W, H, maxShapeSize, minShapeSize);
}
#Override
public void start(final Stage stage) {
Pane pane = new Pane();
pane.setPrefSize(W, H);
for (int i = 0; i < N_SHAPES; i++) {
pane.getChildren().add(machine.randomShape());
}
pane.setOnMouseClicked(event -> {
Node node = findNearestNode(pane.getChildren(), event.getX(), event.getY());
System.out.println("Found: " + node + " at " + event.getX() + "," + event.getY());
highlightSelected(node, pane.getChildren());
});
final Group group = new Group(
pane
);
Parent zoomPane = createZoomPane(group);
VBox layout = new VBox();
layout.getChildren().setAll(createMenuBar(stage, group), zoomPane);
VBox.setVgrow(zoomPane, Priority.ALWAYS);
Scene scene = new Scene(layout);
stage.setTitle("Zoomy");
stage.getIcons().setAll(new Image(APP_ICON));
stage.setScene(scene);
stage.show();
}
private Parent createZoomPane(final Group group) {
final double SCALE_DELTA = 1.1;
final StackPane zoomPane = new StackPane();
zoomPane.getChildren().add(group);
final ScrollPane scroller = new ScrollPane();
final Group scrollContent = new Group(zoomPane);
scroller.setContent(scrollContent);
scroller.viewportBoundsProperty().addListener(new ChangeListener<Bounds>() {
#Override
public void changed(ObservableValue<? extends Bounds> observable,
Bounds oldValue, Bounds newValue) {
zoomPane.setMinSize(newValue.getWidth(), newValue.getHeight());
}
});
scroller.setPrefViewportWidth(256);
scroller.setPrefViewportHeight(256);
zoomPane.setOnScroll(new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
event.consume();
if (event.getDeltaY() == 0) {
return;
}
double scaleFactor = (event.getDeltaY() > 0) ? SCALE_DELTA
: 1 / SCALE_DELTA;
// amount of scrolling in each direction in scrollContent coordinate
// units
Point2D scrollOffset = figureScrollOffset(scrollContent, scroller);
group.setScaleX(group.getScaleX() * scaleFactor);
group.setScaleY(group.getScaleY() * scaleFactor);
// move viewport so that old center remains in the center after the
// scaling
repositionScroller(scrollContent, scroller, scaleFactor, scrollOffset);
}
});
// Panning via drag....
final ObjectProperty<Point2D> lastMouseCoordinates = new SimpleObjectProperty<Point2D>();
scrollContent.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
lastMouseCoordinates.set(new Point2D(event.getX(), event.getY()));
}
});
scrollContent.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
double deltaX = event.getX() - lastMouseCoordinates.get().getX();
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
double deltaH = deltaX * (scroller.getHmax() - scroller.getHmin()) / extraWidth;
double desiredH = scroller.getHvalue() - deltaH;
scroller.setHvalue(Math.max(0, Math.min(scroller.getHmax(), desiredH)));
double deltaY = event.getY() - lastMouseCoordinates.get().getY();
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
double deltaV = deltaY * (scroller.getHmax() - scroller.getHmin()) / extraHeight;
double desiredV = scroller.getVvalue() - deltaV;
scroller.setVvalue(Math.max(0, Math.min(scroller.getVmax(), desiredV)));
}
});
return scroller;
}
private Point2D figureScrollOffset(Node scrollContent, ScrollPane scroller) {
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
double hScrollProportion = (scroller.getHvalue() - scroller.getHmin()) / (scroller.getHmax() - scroller.getHmin());
double scrollXOffset = hScrollProportion * Math.max(0, extraWidth);
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
double vScrollProportion = (scroller.getVvalue() - scroller.getVmin()) / (scroller.getVmax() - scroller.getVmin());
double scrollYOffset = vScrollProportion * Math.max(0, extraHeight);
return new Point2D(scrollXOffset, scrollYOffset);
}
private void repositionScroller(Node scrollContent, ScrollPane scroller, double scaleFactor, Point2D scrollOffset) {
double scrollXOffset = scrollOffset.getX();
double scrollYOffset = scrollOffset.getY();
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
if (extraWidth > 0) {
double halfWidth = scroller.getViewportBounds().getWidth() / 2;
double newScrollXOffset = (scaleFactor - 1) * halfWidth + scaleFactor * scrollXOffset;
scroller.setHvalue(scroller.getHmin() + newScrollXOffset * (scroller.getHmax() - scroller.getHmin()) / extraWidth);
} else {
scroller.setHvalue(scroller.getHmin());
}
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
if (extraHeight > 0) {
double halfHeight = scroller.getViewportBounds().getHeight() / 2;
double newScrollYOffset = (scaleFactor - 1) * halfHeight + scaleFactor * scrollYOffset;
scroller.setVvalue(scroller.getVmin() + newScrollYOffset * (scroller.getVmax() - scroller.getVmin()) / extraHeight);
} else {
scroller.setHvalue(scroller.getHmin());
}
}
private SVGPath createCurve() {
SVGPath ellipticalArc = new SVGPath();
ellipticalArc.setContent("M10,150 A15 15 180 0 1 70 140 A15 25 180 0 0 130 130 A15 55 180 0 1 190 120");
ellipticalArc.setStroke(Color.LIGHTGREEN);
ellipticalArc.setStrokeWidth(4);
ellipticalArc.setFill(null);
return ellipticalArc;
}
private SVGPath createStar() {
SVGPath star = new SVGPath();
star.setContent("M100,10 L100,10 40,180 190,60 10,60 160,180 z");
star.setStrokeLineJoin(StrokeLineJoin.ROUND);
star.setStroke(Color.BLUE);
star.setFill(Color.DARKBLUE);
star.setStrokeWidth(4);
return star;
}
private MenuBar createMenuBar(final Stage stage, final Group group) {
Menu fileMenu = new Menu("_File");
MenuItem exitMenuItem = new MenuItem("E_xit");
exitMenuItem.setGraphic(new ImageView(new Image(CLOSE_ICON)));
exitMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
stage.close();
}
});
fileMenu.getItems().setAll(exitMenuItem);
Menu zoomMenu = new Menu("_Zoom");
MenuItem zoomResetMenuItem = new MenuItem("Zoom _Reset");
zoomResetMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.ESCAPE));
zoomResetMenuItem.setGraphic(new ImageView(new Image(ZOOM_RESET_ICON)));
zoomResetMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
group.setScaleX(1);
group.setScaleY(1);
}
});
MenuItem zoomInMenuItem = new MenuItem("Zoom _In");
zoomInMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.I));
zoomInMenuItem.setGraphic(new ImageView(new Image(ZOOM_IN_ICON)));
zoomInMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
group.setScaleX(group.getScaleX() * 1.5);
group.setScaleY(group.getScaleY() * 1.5);
}
});
MenuItem zoomOutMenuItem = new MenuItem("Zoom _Out");
zoomOutMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.O));
zoomOutMenuItem.setGraphic(new ImageView(new Image(ZOOM_OUT_ICON)));
zoomOutMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
group.setScaleX(group.getScaleX() * 1 / 1.5);
group.setScaleY(group.getScaleY() * 1 / 1.5);
}
});
zoomMenu.getItems().setAll(zoomResetMenuItem, zoomInMenuItem,
zoomOutMenuItem);
MenuBar menuBar = new MenuBar();
menuBar.getMenus().setAll(fileMenu, zoomMenu);
return menuBar;
}
private void highlightSelected(Node selected, ObservableList<Node> children) {
for (Node node : children) {
node.setEffect(null);
}
if (selected != null) {
selected.setEffect(new DropShadow(10, Color.YELLOW));
}
}
private Node findNearestNode(ObservableList<Node> nodes, double x, double y) {
Point2D pClick = new Point2D(x, y);
Node nearestNode = null;
double closestDistance = Double.POSITIVE_INFINITY;
for (Node node : nodes) {
Bounds bounds = node.getBoundsInParent();
Point2D[] corners = new Point2D[]{
new Point2D(bounds.getMinX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMaxY()),
new Point2D(bounds.getMinX(), bounds.getMaxY()),
};
for (Point2D pCompare : corners) {
double nextDist = pClick.distance(pCompare);
if (nextDist < closestDistance) {
closestDistance = nextDist;
nearestNode = node;
}
}
}
return nearestNode;
}
// icons source from:
// http://www.iconarchive.com/show/soft-scraps-icons-by-deleket.html
// icon license: CC Attribution-Noncommercial-No Derivate 3.0 =?
// http://creativecommons.org/licenses/by-nc-nd/3.0/
// icon Commercial usage: Allowed (Author Approval required -> Visit artist
// website for details).
public static final String APP_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/128/Zoom-icon.png";
public static final String ZOOM_RESET_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-icon.png";
public static final String ZOOM_OUT_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-Out-icon.png";
public static final String ZOOM_IN_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-In-icon.png";
public static final String CLOSE_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Button-Close-icon.png";
}
I am relatively new to property bindings and I am looking for some high-level advice on how to approach a design problem, which I will try to describe a simple example of here.
Problem description
The goal in this example is to allow the user to specify a box/rectangular region interactively in a pannable and zoomable 2D space. The 2D screen-space in which the box is depicted, maps to a 2D "real-space" (e.g. voltage vs time cartesian space, or GPS, or whatever). The user should be able to zoom/pan his viewport vertically/horizontally at any time, thereby changing the mapping between these two spaces.
screen-space <-------- user-adjustable mapping --------> real-space
The user specifies the rectangle in his viewport by dragging borders/corners, as in this demo:
class InteractiveHandle extends Rectangle {
private final Cursor hoverCursor;
private final Cursor activeCursor;
private final DoubleProperty centerXProperty = new SimpleDoubleProperty();
private final DoubleProperty centerYProperty = new SimpleDoubleProperty();
InteractiveHandle(DoubleProperty x, DoubleProperty y, double w, double h) {
super();
centerXProperty.bindBidirectional(x);
centerYProperty.bindBidirectional(y);
widthProperty().set(w);
heightProperty().set(h);
hoverCursor = Cursor.MOVE;
activeCursor = Cursor.MOVE;
bindRect();
enableDrag(true,true);
}
InteractiveHandle(DoubleProperty x, ObservableDoubleValue y, double w, ObservableDoubleValue h) {
super();
centerXProperty.bindBidirectional(x);
centerYProperty.bind(y);
widthProperty().set(w);
heightProperty().bind(h);
hoverCursor = Cursor.H_RESIZE;
activeCursor = Cursor.H_RESIZE;
bindRect();
enableDrag(true,false);
}
InteractiveHandle(ObservableDoubleValue x, DoubleProperty y, ObservableDoubleValue w, double h) {
super();
centerXProperty.bind(x);
centerYProperty.bindBidirectional(y);
widthProperty().bind(w);
heightProperty().set(h);
hoverCursor = Cursor.V_RESIZE;
activeCursor = Cursor.V_RESIZE;
bindRect();
enableDrag(false,true);
}
InteractiveHandle(ObservableDoubleValue x, ObservableDoubleValue y, ObservableDoubleValue w, ObservableDoubleValue h) {
super();
centerXProperty.bind(x);
centerYProperty.bind(y);
widthProperty().bind(w);
heightProperty().bind(h);
hoverCursor = Cursor.DEFAULT;
activeCursor = Cursor.DEFAULT;
bindRect();
enableDrag(false,false);
}
private void bindRect(){
xProperty().bind(centerXProperty.subtract(widthProperty().divide(2)));
yProperty().bind(centerYProperty.subtract(heightProperty().divide(2)));
}
//make a node movable by dragging it around with the mouse.
private void enableDrag(boolean xDraggable, boolean yDraggable) {
final Delta dragDelta = new Delta();
setOnMousePressed((MouseEvent mouseEvent) -> {
// record a delta distance for the drag and drop operation.
dragDelta.x = centerXProperty.get() - mouseEvent.getX();
dragDelta.y = centerYProperty.get() - mouseEvent.getY();
getScene().setCursor(activeCursor);
});
setOnMouseReleased((MouseEvent mouseEvent) -> {
getScene().setCursor(hoverCursor);
});
setOnMouseDragged((MouseEvent mouseEvent) -> {
if(xDraggable){
double newX = mouseEvent.getX() + dragDelta.x;
if (newX > 0 && newX < getScene().getWidth()) {
centerXProperty.set(newX);
}
}
if(yDraggable){
double newY = mouseEvent.getY() + dragDelta.y;
if (newY > 0 && newY < getScene().getHeight()) {
centerYProperty.set(newY);
}
}
});
setOnMouseEntered((MouseEvent mouseEvent) -> {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(hoverCursor);
}
});
setOnMouseExited((MouseEvent mouseEvent) -> {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.DEFAULT);
}
});
}
//records relative x and y co-ordinates.
private class Delta { double x, y; }
}
public class InteractiveBox extends Group {
private static final double sideHandleWidth = 2;
private static final double cornerHandleSize = 4;
private static final double minHandleFraction = 0.5;
private static final double maxCornerClearance = 6;
private static final double handleInset = 2;
private final Rectangle rectangle;
private final InteractiveHandle ihLeft;
private final InteractiveHandle ihTop;
private final InteractiveHandle ihRight;
private final InteractiveHandle ihBottom;
private final InteractiveHandle ihTopLeft;
private final InteractiveHandle ihTopRight;
private final InteractiveHandle ihBottomLeft;
private final InteractiveHandle ihBottomRight;
InteractiveBox(DoubleProperty xMin, DoubleProperty yMin, DoubleProperty xMax, DoubleProperty yMax){
super();
rectangle = new Rectangle();
rectangle.widthProperty().bind(xMax.subtract(xMin));
rectangle.heightProperty().bind(yMax.subtract(yMin));
rectangle.xProperty().bind(xMin);
rectangle.yProperty().bind(yMin);
DoubleBinding xMid = xMin.add(xMax).divide(2);
DoubleBinding yMid = yMin.add(yMax).divide(2);
DoubleBinding hx = (DoubleBinding) Bindings.max(
rectangle.widthProperty().multiply(minHandleFraction)
,rectangle.widthProperty().subtract(maxCornerClearance*2)
);
DoubleBinding vx = (DoubleBinding) Bindings.max(
rectangle.heightProperty().multiply(minHandleFraction)
,rectangle.heightProperty().subtract(maxCornerClearance*2)
);
ihTopLeft = new InteractiveHandle(xMin,yMax,cornerHandleSize,cornerHandleSize);
ihTopRight = new InteractiveHandle(xMax,yMax,cornerHandleSize,cornerHandleSize);
ihBottomLeft = new InteractiveHandle(xMin,yMin,cornerHandleSize,cornerHandleSize);
ihBottomRight = new InteractiveHandle(xMax,yMin,cornerHandleSize,cornerHandleSize);
ihLeft = new InteractiveHandle(xMin,yMid,sideHandleWidth,vx);
ihTop = new InteractiveHandle(xMid,yMax,hx,sideHandleWidth);
ihRight = new InteractiveHandle(xMax,yMid,sideHandleWidth,vx);
ihBottom = new InteractiveHandle(xMid,yMin,hx,sideHandleWidth);
style(ihLeft);
style(ihTop);
style(ihRight);
style(ihBottom);
style(ihTopLeft);
style(ihTopRight);
style(ihBottomLeft);
style(ihBottomRight);
getChildren().addAll(rectangle
,ihTopLeft, ihTopRight, ihBottomLeft, ihBottomRight
,ihLeft, ihTop, ihRight, ihBottom
);
rectangle.setFill(Color.ALICEBLUE);
rectangle.setStroke(Color.LIGHTGRAY);
rectangle.setStrokeWidth(2);
rectangle.setStrokeType(StrokeType.CENTERED);
}
private void style(InteractiveHandle ih){
ih.setStroke(Color.TRANSPARENT);
ih.setStrokeWidth(handleInset);
ih.setStrokeType(StrokeType.OUTSIDE);
}
}
public class Summoner extends Application {
DoubleProperty x = new SimpleDoubleProperty(50);
DoubleProperty y = new SimpleDoubleProperty(50);
DoubleProperty xMax = new SimpleDoubleProperty(100);
DoubleProperty yMax = new SimpleDoubleProperty(100);
#Override
public void start(Stage primaryStage) {
InteractiveBox box = new InteractiveBox(x,y,xMax,yMax);
Pane root = new Pane();
root.getChildren().add(box);
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
After the rectangle has been specified by the user, its coordinates (in real-space) are passed on to or read by a different part of the program.
My rationale
My first instinct was to use the built-in scale/translate properties in JavaFX nodes to implement the mapping, but we want borders and handles to have a consistent size/appearance regardless of zoom-state; zooming should only embiggen the conceptual rectangle itself, not thicken the borders or corner-handles.
(In the following, arrows represent causality/influence/dependency. For example, A ---> B could mean property B is bound to property A (or it could mean that event-handler A sets property B), and <-----> could represent a bidirectional binding. A multi-tailed arrow such as --+--> could represent a binding that depends on multiple input observables.)
So my question became: which of the following should I do?
real-space-properties ---+--> screen-space-properties
real-space-properties <--+--- screen-space properties
or something different, using <---->
On the one hand, we have mouse events and the rendered rectangle itself in screen-space. This argues for a self-contained interactive rectangle (whose screen-space position/dimension properties we can observe (as well as manipulate, if we wanted to) externally) as per the demo above.
mouse events -----> screen-space properties ------> depicted rectangle
|
|
--------> real-space properties -----> API
On the other hand, when the user adjusts pan/zoom, we want the rectangle's properties in real-space (not screen-space) to be preserved. This argues for binding the screen-space properties to real-space properties using pan&zoom-state properties:
pan/zoom properties
|
|
real-space properties ---+--> screen-space properties ------> depicted rectangle
|
|
-------> API
If I try to put together both approaches above, I run into a problem:
mouse events
|
pan/zoom properties |
| |
| v
real-space properties <--+--> screen-space properties ------> depicted rectangle
| *
|
-------> API
This diagram makes a lot of sense to me, but I don't think the kind of "bidirectional" 3-way binding at * is possible, directly. But is there perhaps a simple way to emulate/work around it? Or should I take an entirely different approach?
Here's an example of a rectangle on a zoom & pannable pane with a constant stroke width. You just have to define the scale factor as a property of the pane, bind a it to a property in the calling class and divide that into a property that's bound to the strokewidth of the rectangle.
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ZoomAndPanExample extends Application {
private ScrollPane scrollPane = new ScrollPane();
private final DoubleProperty zoomProperty = new SimpleDoubleProperty(1.0d);
private final DoubleProperty strokeWidthProperty = new SimpleDoubleProperty(1.0d);
private final DoubleProperty deltaY = new SimpleDoubleProperty(0.0d);
private final Group group = new Group();
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) {
scrollPane.setPannable(true);
scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);
AnchorPane.setTopAnchor(scrollPane, 10.0d);
AnchorPane.setRightAnchor(scrollPane, 10.0d);
AnchorPane.setBottomAnchor(scrollPane, 10.0d);
AnchorPane.setLeftAnchor(scrollPane, 10.0d);
AnchorPane root = new AnchorPane();
Rectangle rect = new Rectangle(80, 60);
rect.setStroke(Color.NAVY);
rect.setFill(Color.web("#000080", 0.2));
rect.setStrokeType(StrokeType.INSIDE);
rect.strokeWidthProperty().bind(strokeWidthProperty.divide(zoomProperty));
group.getChildren().add(rect);
// create canvas
PanAndZoomPane panAndZoomPane = new PanAndZoomPane();
zoomProperty.bind(panAndZoomPane.myScale);
deltaY.bind(panAndZoomPane.deltaY);
panAndZoomPane.getChildren().add(group);
SceneGestures sceneGestures = new SceneGestures(panAndZoomPane);
scrollPane.setContent(panAndZoomPane);
panAndZoomPane.toBack();
scrollPane.addEventFilter( MouseEvent.MOUSE_CLICKED, sceneGestures.getOnMouseClickedEventHandler());
scrollPane.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scrollPane.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scrollPane.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
root.getChildren().add(scrollPane);
Scene scene = new Scene(root, 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
class PanAndZoomPane extends Pane {
public static final double DEFAULT_DELTA = 1.3d;
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public DoubleProperty deltaY = new SimpleDoubleProperty(0.0);
private Timeline timeline;
public PanAndZoomPane() {
this.timeline = new Timeline(60);
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
}
public double getScale() {
return myScale.get();
}
public void setScale( double scale) {
myScale.set(scale);
}
public void setPivot( double x, double y, double scale) {
// note: pivot value must be untransformed, i. e. without scaling
// timeline that scales and moves the node
timeline.getKeyFrames().clear();
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.millis(200), new KeyValue(translateXProperty(), getTranslateX() - x)),
new KeyFrame(Duration.millis(200), new KeyValue(translateYProperty(), getTranslateY() - y)),
new KeyFrame(Duration.millis(200), new KeyValue(myScale, scale))
);
timeline.play();
}
/**
* fit the rectangle to the width of the window
*/
public void fitWidth () {
double scale = getParent().getLayoutBounds().getMaxX()/getLayoutBounds().getMaxX();
double oldScale = getScale();
double f = (scale / oldScale)-1;
double dx = getTranslateX() - getBoundsInParent().getMinX() - getBoundsInParent().getWidth()/2;
double dy = getTranslateY() - getBoundsInParent().getMinY() - getBoundsInParent().getHeight()/2;
double newX = f*dx + getBoundsInParent().getMinX();
double newY = f*dy + getBoundsInParent().getMinY();
setPivot(newX, newY, scale);
}
public void resetZoom () {
double scale = 1.0d;
double x = getTranslateX();
double y = getTranslateY();
setPivot(x, y, scale);
}
public double getDeltaY() {
return deltaY.get();
}
public void setDeltaY( double dY) {
deltaY.set(dY);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
public class SceneGestures {
private DragContext sceneDragContext = new DragContext();
PanAndZoomPane panAndZoomPane;
public SceneGestures( PanAndZoomPane canvas) {
this.panAndZoomPane = canvas;
}
public EventHandler<MouseEvent> getOnMouseClickedEventHandler() {
return onMouseClickedEventHandler;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
public EventHandler<ScrollEvent> getOnScrollEventHandler() {
return onScrollEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
sceneDragContext.mouseAnchorX = event.getX();
sceneDragContext.mouseAnchorY = event.getY();
sceneDragContext.translateAnchorX = panAndZoomPane.getTranslateX();
sceneDragContext.translateAnchorY = panAndZoomPane.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
panAndZoomPane.setTranslateX(sceneDragContext.translateAnchorX + event.getX() - sceneDragContext.mouseAnchorX);
panAndZoomPane.setTranslateY(sceneDragContext.translateAnchorY + event.getY() - sceneDragContext.mouseAnchorY);
event.consume();
}
};
/**
* Mouse wheel handler: zoom to pivot point
*/
private EventHandler<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double delta = PanAndZoomPane.DEFAULT_DELTA;
double scale = panAndZoomPane.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
panAndZoomPane.setDeltaY(event.getDeltaY());
if (panAndZoomPane.deltaY.get() < 0) {
scale /= delta;
} else {
scale *= delta;
}
double f = (scale / oldScale)-1;
double dx = (event.getX() - (panAndZoomPane.getBoundsInParent().getWidth()/2 + panAndZoomPane.getBoundsInParent().getMinX()));
double dy = (event.getY() - (panAndZoomPane.getBoundsInParent().getHeight()/2 + panAndZoomPane.getBoundsInParent().getMinY()));
panAndZoomPane.setPivot(f*dx, f*dy, scale);
event.consume();
}
};
/**
* Mouse click handler
*/
private EventHandler<MouseEvent> onMouseClickedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.getButton().equals(MouseButton.PRIMARY)) {
if (event.getClickCount() == 2) {
panAndZoomPane.resetZoom();
}
}
if (event.getButton().equals(MouseButton.SECONDARY)) {
if (event.getClickCount() == 2) {
panAndZoomPane.fitWidth();
}
}
}
};
}
}
I already have implemented display of binary search tree . Here's the code , which paints the
binary tree in a jpanel .
public void paint(Graphics g) {
super.paint(g);
System.out.println(" in paint");
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
int num = bst.size;
int y = 25;
int nodes = 1;
int level = 1;
int length = getWidth();
Queue<Node> q = new LinkedList<Node>();
Queue<Integer> q2 = new LinkedList<Integer>();
q.add(bst.root);
while (num > 0) {
int pX = (int) Math.round(length / (2.0 * nodes));
int x = pX;
for (int i = 0; i < nodes; i++) {
Node n = q.poll();
//
if (n != null) {
num--;
System.out.println(x);
g2.setColor(Color.BLUE);
String str = n.value + "";
System.out.println(str);
//Font f = Font.getFont(str);
int width = str.length();
g2.setColor(Color.YELLOW);
g2.fillOval(x, y, (30 - 2 * level)+width*3, (30 - 2 * level));
g2.setColor(Color.black);
g2.drawString(n.value + "", x + 10 - level, y + 15);
g2.setColor(Color.black);
if (n.left == null)
q.add(null);
else
q.add(n.left);
if (n.right == null)
q.add(null);
else
q.add(n.right);
if (level != 1) {
int xx = q2.poll();
int yy = q2.poll();
g2.drawLine(xx+width*2, yy, x + (15 - 1 * level)+width*2, y);
}
} else {
q2.poll();
q2.poll();
q.add(null);
q.add(null);
}
q2.add(x);
q2.add(y + 15 - level);
q2.add(x + 30 - 2 * level);
q2.add(y + 15 - level);
x += 2 * pX;
}
y += 40;
nodes = 1 << level;
level++;
}
Now as i insert nodes into my tree , I want the parent nodes to change color of the new node progressively , and then ultimate join as a child .
or
the new node to be inserted moves along the path of it's parent . or something similar
Here's an example :
I have no idea how to accomplish that , with timer or likewise .
Okay, this took a little longer then I wanted (10 month olds don't have any patience)
The basic concept revolves around the idea that you need to change from one state to another over a period of time.
Given a start time and the current time, we can calculate the amount of time that the animation has been running, and given the total animation time, the current progress.
With this (and some clever maths) we can calculate the current state from our start state towards our target state.
I've done movement as well, so that may be a little over kill, but the basic premise remains the same.
I place stateful information about the nodes that need to change in animation properties class and use a javax.swing.Timer to tick over the animation (at a reasonably steady rate). I then update the state of each node as required and repaint the screen.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.text.NumberFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import javax.management.StringValueExp;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class AnimateNode {
public static void main(String[] args) {
new AnimateNode();
}
public AnimateNode() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new NodePane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public interface Node {
public void paint(JComponent parent, Graphics2D g2d);
public void setColor(Color color);
public Color getColor();
public Node getParent();
public Node getLeft();
public Node getRight();
public void setLeftNode(Node node);
public void setRightNode(Node node);
public Point getLocation();
public void setLocation(Point p);
}
public class DefaultNode implements Node {
private int number;
private Node parent;
private Node left;
private Node right;
private Point location;
private Color color;
public DefaultNode(int number, Node parent) {
this.parent = parent;
color = UIManager.getColor("Panel.background");
this.number = number;
}
public void setLeftNode(Node left) {
this.left = left;
}
public void setRightNode(Node right) {
this.right = right;
}
public Node getParent() {
return parent;
}
public Node getLeft() {
return left;
}
public Node getRight() {
return right;
}
#Override
public Point getLocation() {
return location;
}
#Override
public void setLocation(Point location) {
this.location = location;
}
#Override
public void paint(JComponent parent, Graphics2D g2d) {
FontMetrics fm = g2d.getFontMetrics();
int radius = fm.getHeight();
Point p = getLocation();
int x = p.x - (radius / 2);
int y = p.y - (radius / 2);
Ellipse2D node = new Ellipse2D.Float(x, y, radius, radius);
g2d.setColor(getColor());
g2d.fill(node);
g2d.setColor(Color.GRAY);
g2d.draw(node);
String text = String.valueOf(number);
x = x + ((radius - fm.stringWidth(text)) / 2);
y = y + (((radius - fm.getHeight()) / 2) + fm.getAscent());
g2d.drawString(text, x, y);
}
#Override
public void setColor(Color color) {
this.color = color;
}
#Override
public Color getColor() {
return color;
}
#Override
public String toString() {
return number + " # " + getLocation();
}
}
public class AnimationProperties {
private Point startPoint;
private Point targetPoint;
private Color startColor;
private Color endColor;
private Node node;
public AnimationProperties(Node node) {
this.node = node;
}
public Node getNode() {
return node;
}
public void setTargetColor(Color endColor) {
this.endColor = endColor;
}
public void setStartColor(Color startColor) {
this.startColor = startColor;
}
public void setStartPoint(Point startPoint) {
this.startPoint = startPoint;
}
public void setTargetPoint(Point targetPoint) {
this.targetPoint = targetPoint;
}
public Color getTargetColor() {
return endColor;
}
public Color getStartColor() {
return startColor;
}
public Point getStartPoint() {
return startPoint;
}
public Point getTargetPoint() {
return targetPoint;
}
public Point getLocation(float progress) {
return calculateProgress(getStartPoint(), getTargetPoint(), progress);
}
public Color getColor(float progress) {
return blend(getStartColor(), getTargetColor(), 1f - progress);
}
public void update(float progress) {
node.setLocation(getLocation(progress));
node.setColor(getColor(progress));
}
}
public class NodePane extends JPanel {
private int number;
private Node root;
private Map<Node, AnimationProperties> aniProperties;
private Timer animationTimer;
private Timer startTimer;
private long startTime;
private int runTime = 1000;
public NodePane() {
aniProperties = new HashMap<>(25);
root = addLeftNode(null);
root.setColor(getBackground());
addMouseListener(new MouseAdapter() {
private Random rand;
#Override
public void mouseClicked(MouseEvent e) {
generateNextNode(root);
revalidate();
// repaint();
}
protected void generateNextNode(Node parent) {
Node child = null;
if (rand == null) {
rand = new Random(System.currentTimeMillis());
}
boolean left = rand.nextBoolean();
if (left) {
child = parent.getLeft();
} else {
child = parent.getRight();
}
if (child == null) {
if (left) {
addLeftNode(parent);
} else {
addRightNode(parent);
}
} else {
generateNextNode(child);
}
}
});
startTimer = new Timer(250, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
stopAnimation();
startTime = -1;
animationTimer.start();
}
});
startTimer.setRepeats(false);
animationTimer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (startTime < 0) {
startTime = System.currentTimeMillis();
}
float progress = 1f;
long duration = System.currentTimeMillis() - startTime;
if (duration >= runTime) {
((Timer) e.getSource()).stop();
} else {
progress = (float) duration / (float) runTime;
}
for (AnimationProperties ap : aniProperties.values()) {
ap.update(progress);
}
repaint();
if (progress == 1f) {
aniProperties.clear();
}
}
});
animationTimer.setRepeats(true);
animationTimer.setCoalesce(true);
}
protected void stopAnimation() {
if (animationTimer.isRunning()) {
animationTimer.stop();
for (AnimationProperties ap : aniProperties.values()) {
Node node = ap.getNode();
ap.setStartColor(node.getColor());
ap.setStartPoint(node.getLocation());
}
}
}
public Point getStartPoint(Node node) {
Point startPoint = node.getLocation();
while (startPoint == null) {
node = node.getParent();
startPoint = node.getLocation();
}
return startPoint;
}
protected void layoutNode(Node node, int x, int y) {
if (node != null) {
FontMetrics fm = getFontMetrics(getFont());
int nodeHeight = fm.getHeight();
if (node.getParent() != null) {
Point p = new Point(x, y);
Point sp = getStartPoint(node);
if (node.getLocation() == null) {
System.out.println("new node " + node);
}
if (node.getLocation() == null || !p.equals(node.getLocation())) {
AnimationProperties ap = new AnimationProperties(node);
ap.setStartColor(node.getColor());
ap.setTargetColor(getBackground());
ap.setStartPoint(sp);
ap.setTargetPoint(new Point(x, y));
node.setLocation(sp);
aniProperties.put(node, ap);
System.out.println("New Node to " + node);
} else {
aniProperties.remove(node);
}
} else {
nodeHeight *= 2;
}
layoutNode(node.getLeft(), x - nodeHeight, y + nodeHeight);
layoutNode(node.getRight(), x + nodeHeight, y + nodeHeight);
}
}
#Override
public void doLayout() {
System.out.println("DoLayout");
stopAnimation();
FontMetrics fm = getFontMetrics(getFont());
int nodeHeight = fm.getHeight();
int x = getWidth() / 2;
int y = nodeHeight;
if (root != null) {
root.setLocation(new Point(x, y));
layoutNode(root, x, y);
// Node node = root.getLeft();
// while (node != null) {
// x -= nodeHeight;
// y += nodeHeight;
// layout(node, x, y);
// node = node.getLeft();
// }
// node = root.getRight();
// x = getWidth() / 2;
// y = nodeHeight;
// while (node != null) {
// x += nodeHeight;
// y += nodeHeight;
// layout(node, x, y);
// node = node.getRight();
// }
}
startTimer.restart();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
protected Node createNode(Node parent) {
DefaultNode child = new DefaultNode(++number, parent);
child.setColor(Color.GREEN);
System.out.println("Create new node " + child);
return child;
}
protected Node addLeftNode(Node parent) {
Node node = createNode(parent);
if (parent != null) {
System.out.println("Add " + node + " to left of " + parent);
parent.setLeftNode(node);
}
return node;
}
protected Node addRightNode(Node parent) {
Node node = createNode(parent);
if (parent != null) {
System.out.println("Add " + node + " to right of " + parent);
parent.setRightNode(node);
}
return node;
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (root != null) {
Graphics2D g2d = (Graphics2D) g.create();
paintConnectors(root, g2d);
paintNode(root, g2d);
g2d.dispose();
}
}
protected void paintNode(Node node, Graphics2D g2d) {
if (node != null && node.getLocation() != null) {
node.paint(this, g2d);
paintNode(node.getLeft(), g2d);
paintNode(node.getRight(), g2d);
}
}
protected void paintConnectors(Node node, Graphics2D g2d) {
if (node != null && node.getLocation() != null) {
Node parent = node.getParent();
if (parent != null) {
g2d.setColor(Color.GRAY);
if (parent.getLocation() != null && node.getLocation() != null) {
g2d.draw(new Line2D.Float(parent.getLocation(), node.getLocation()));
}
}
paintConnectors(node.getLeft(), g2d);
paintConnectors(node.getRight(), g2d);
}
}
}
public static Point calculateProgress(Point startPoint, Point targetPoint, double progress) {
Point point = new Point();
if (startPoint != null && targetPoint != null) {
point.x = calculateProgress(startPoint.x, targetPoint.x, progress);
point.y = calculateProgress(startPoint.y, targetPoint.y, progress);
}
return point;
}
public static int calculateProgress(int startValue, int endValue, double fraction) {
int value = 0;
int distance = endValue - startValue;
value = (int) Math.round((double) distance * fraction);
value += startValue;
return value;
}
public static Color calculateProgress(Color start, Color target, double progress) {
return blend(start, target, progress);
}
public static Color blend(Color color1, Color color2, double ratio) {
float r = (float) ratio;
float ir = (float) 1.0 - r;
float rgb1[] = new float[3];
float rgb2[] = new float[3];
color1.getColorComponents(rgb1);
color2.getColorComponents(rgb2);
float red = rgb1[0] * r + rgb2[0] * ir;
float green = rgb1[1] * r + rgb2[1] * ir;
float blue = rgb1[2] * r + rgb2[2] * ir;
if (red < 0) {
red = 0;
} else if (red > 255) {
red = 255;
}
if (green < 0) {
green = 0;
} else if (green > 255) {
green = 255;
}
if (blue < 0) {
blue = 0;
} else if (blue > 255) {
blue = 255;
}
Color color = null;
try {
color = new Color(red, green, blue);
} catch (IllegalArgumentException exp) {
NumberFormat nf = NumberFormat.getNumberInstance();
System.err.println(nf.format(red) + "; " + nf.format(green) + "; " + nf.format(blue));
}
return color;
}
}
Update with simple example ;)
Okay, this is a simple example. Basically, it just blinks the node...
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import java.text.NumberFormat;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class BlinkNode {
public static void main(String[] args) {
new BlinkNode();
}
public BlinkNode() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
// Animation stuff
private Timer aniTimer;
// The amount of time that each animation cycle plays for
// in millis
private int aniRunTime = 1000;
// The time the animation was started
private long startTime = -1;
// Our color ranges, where to start and where
// we want to get to and the current state...
private Color startColor;
private Color targetColor;
private Color color;
public TestPane() {
// Initial state
startColor = getBackground();
targetColor = Color.GREEN;
color = startColor;
aniTimer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
// Set the start time it hasn't already
if (startTime < 0) {
startTime = System.currentTimeMillis();
}
// We're always finished if we run over time...
float progress = 1f;
// Calculate the duration of play
long duration = System.currentTimeMillis() - startTime;
// Have we reached the end yet??
if (duration >= aniRunTime) {
// Reset the start time, this allows the
// animation to cycle. Normally you would stop
// the timer, see the previous example
startTime = -1;
// Swap the start and target colors...
Color tmp = startColor;
startColor = targetColor;
targetColor = tmp;
color = startColor;
} else {
// Calculate the progress
progress = (float) duration / (float) aniRunTime;
// Blend the colors
color = blend(startColor, targetColor, 1f - progress);
}
// update the ui
repaint();
}
});
aniTimer.setRepeats(true);
aniTimer.setCoalesce(true);
aniTimer.start();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
int x = (getWidth() - 20) / 2;
int y = (getHeight() - 20) / 2;
g2d.setColor(color);
Ellipse2D node = new Ellipse2D.Float(x, y, 20, 20);
g2d.fill(node);
g2d.setColor(Color.GRAY);
g2d.draw(node);
g2d.dispose();
}
}
public static Color blend(Color color1, Color color2, double ratio) {
float r = (float) ratio;
float ir = (float) 1.0 - r;
float rgb1[] = new float[3];
float rgb2[] = new float[3];
color1.getColorComponents(rgb1);
color2.getColorComponents(rgb2);
float red = rgb1[0] * r + rgb2[0] * ir;
float green = rgb1[1] * r + rgb2[1] * ir;
float blue = rgb1[2] * r + rgb2[2] * ir;
if (red < 0) {
red = 0;
} else if (red > 255) {
red = 255;
}
if (green < 0) {
green = 0;
} else if (green > 255) {
green = 255;
}
if (blue < 0) {
blue = 0;
} else if (blue > 255) {
blue = 255;
}
Color color = null;
try {
color = new Color(red, green, blue);
} catch (IllegalArgumentException exp) {
NumberFormat nf = NumberFormat.getNumberInstance();
System.err.println(nf.format(red) + "; " + nf.format(green) + "; " + nf.format(blue));
}
return color;
}
}
Conceptually, if each node should have a specific color then each Node instance should have a Color attribute. In the example cited here, the class Node has a number of static updateXxx() methods that traverse the program's (simpler) model, updating nodes as specified. In particular, updateColor() sets each element's Color field to the specified color. Your implementation of paintComponent() can do something similar.
Addendum: As #MadP comments, javax.swing.Timer is well suited to periodic GUI updates, as the timer's actionPerformed() method is executed on the EDT. In this example, the model is updated with each invocation, and the new state is rendered when repaint() (indirectly) calls paintComponent().
I'm starting to learn java programming and I think it's cool to learn java through game development. I know how to draw image and listen to a keypress then move that image. But is it possible to make the image move back and forth to the window while the window is listening to a keypress? Like for example, while the image or object(like spaceship) is moving left to right in the window, then if I press space key, a laser will fire at the bottom of the screen( cool huh :D ). But basically I just want to know how to make the image move left to right while the window is listening to a keypress.
I'm thinking that I will add a key listener to my window then fire an infinite loop to move the image. Or do I need to learn about threading so that another thread will move the object?
Please advise.
Many thanks.
Yep, a Swing Timer and Key Bindings would work well. Here's another example (mine) :)
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
public class AnimationWithKeyBinding {
private static void createAndShowUI() {
AnimationPanel panel = new AnimationPanel(); // the drawing JPanel
JFrame frame = new JFrame("Animation With Key Binding");
frame.getContentPane().add(panel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
createAndShowUI();
}
});
}
}
#SuppressWarnings("serial")
class AnimationPanel extends JPanel {
public static final int SPRITE_WIDTH = 20;
public static final int PANEL_WIDTH = 400;
public static final int PANEL_HEIGHT = 400;
private static final int MAX_MSTATE = 25;
private static final int SPIN_TIMER_PERIOD = 16;
private static final int SPRITE_STEP = 3;
private int mState = 0;
private int mX = (PANEL_WIDTH - SPRITE_WIDTH) / 2;
private int mY = (PANEL_HEIGHT - SPRITE_WIDTH) / 2;
private int oldMX = mX;
private int oldMY = mY;
private boolean moved = false;
// an array of sprite images that are drawn sequentially
private BufferedImage[] spriteImages = new BufferedImage[MAX_MSTATE];
public AnimationPanel() {
// create and start the main animation timer
new Timer(SPIN_TIMER_PERIOD, new SpinTimerListener()).start();
setPreferredSize(new Dimension(PANEL_WIDTH, PANEL_HEIGHT));
setBackground(Color.white);
createSprites(); // create the images
setupKeyBinding();
}
private void setupKeyBinding() {
int condition = JComponent.WHEN_IN_FOCUSED_WINDOW;
InputMap inMap = getInputMap(condition);
ActionMap actMap = getActionMap();
// this uses an enum of Direction that holds ints for the arrow keys
for (Direction direction : Direction.values()) {
int key = direction.getKey();
String name = direction.name();
// add the key bindings for arrow key and shift-arrow key
inMap.put(KeyStroke.getKeyStroke(key, 0), name);
inMap.put(KeyStroke.getKeyStroke(key, InputEvent.SHIFT_DOWN_MASK), name);
actMap.put(name, new MyKeyAction(this, direction));
}
}
// create a bunch of buffered images and place into an array,
// to be displayed sequentially
private void createSprites() {
for (int i = 0; i < spriteImages.length; i++) {
spriteImages[i] = new BufferedImage(SPRITE_WIDTH, SPRITE_WIDTH,
BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = spriteImages[i].createGraphics();
g2.setColor(Color.red);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
double theta = i * Math.PI / (2 * spriteImages.length);
double x = SPRITE_WIDTH * Math.abs(Math.cos(theta)) / 2.0;
double y = SPRITE_WIDTH * Math.abs(Math.sin(theta)) / 2.0;
int x1 = (int) ((SPRITE_WIDTH / 2.0) - x);
int y1 = (int) ((SPRITE_WIDTH / 2.0) - y);
int x2 = (int) ((SPRITE_WIDTH / 2.0) + x);
int y2 = (int) ((SPRITE_WIDTH / 2.0) + y);
g2.drawLine(x1, y1, x2, y2);
g2.drawLine(y1, x2, y2, x1);
g2.dispose();
}
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(spriteImages[mState], mX, mY, null);
}
public void incrementX(boolean right) {
oldMX = mX;
if (right) {
mX = Math.min(getWidth() - SPRITE_WIDTH, mX + SPRITE_STEP);
} else {
mX = Math.max(0, mX - SPRITE_STEP);
}
moved = true;
}
public void incrementY(boolean down) {
oldMY = mY;
if (down) {
mY = Math.min(getHeight() - SPRITE_WIDTH, mY + SPRITE_STEP);
} else {
mY = Math.max(0, mY - SPRITE_STEP);
}
moved = true;
}
public void tick() {
mState = (mState + 1) % MAX_MSTATE;
}
private class SpinTimerListener implements ActionListener {
#Override
public void actionPerformed(ActionEvent e) {
tick();
int delta = 20;
int width = SPRITE_WIDTH + 2 * delta;
int height = width;
// make sure to erase the old image
if (moved) {
int x = oldMX - delta;
int y = oldMY - delta;
repaint(x, y, width, height);
}
int x = mX - delta;
int y = mY - delta;
// draw the new image
repaint(x, y, width, height);
moved = false;
}
}
}
enum Direction {
UP(KeyEvent.VK_UP), DOWN(KeyEvent.VK_DOWN), LEFT(KeyEvent.VK_LEFT), RIGHT(KeyEvent.VK_RIGHT);
private int key;
private Direction(int key) {
this.key = key;
}
public int getKey() {
return key;
}
}
// Actions for the key binding
#SuppressWarnings("serial")
class MyKeyAction extends AbstractAction {
private AnimationPanel draw;
private Direction direction;
public MyKeyAction(AnimationPanel draw, Direction direction) {
this.draw = draw;
this.direction = direction;
}
#Override
public void actionPerformed(ActionEvent e) {
switch (direction) {
case UP:
draw.incrementY(false);
break;
case DOWN:
draw.incrementY(true);
break;
case LEFT:
draw.incrementX(false);
break;
case RIGHT:
draw.incrementX(true);
break;
default:
break;
}
}
}
Here is another example that uses this sprite sheet:
obtained from this site.
Again it's an example of drawing within a JPanel's paintComponent method and using Key Bindings to tell which direction to move.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import javax.imageio.ImageIO;
import javax.swing.*;
#SuppressWarnings("serial")
public class Mcve3 extends JPanel {
private static final int PREF_W = 800;
private static final int PREF_H = 640;
private static final int TIMER_DELAY = 50;
private int spriteX = 400;
private int spriteY = 320;
private SpriteDirection spriteDirection = SpriteDirection.RIGHT;
private MySprite sprite = null;
private Timer timer = null;
public Mcve3() {
try {
sprite = new MySprite(spriteDirection, spriteX, spriteY);
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
setBackground(Color.WHITE);
setKeyBindings(SpriteDirection.LEFT, KeyEvent.VK_LEFT);
setKeyBindings(SpriteDirection.RIGHT, KeyEvent.VK_RIGHT);
setKeyBindings(SpriteDirection.FORWARD, KeyEvent.VK_DOWN);
setKeyBindings(SpriteDirection.AWAY, KeyEvent.VK_UP);
timer = new Timer(TIMER_DELAY, new TimerListener());
timer.start();
}
private void setKeyBindings(SpriteDirection dir, int keyCode) {
int condition = WHEN_IN_FOCUSED_WINDOW;
InputMap inputMap = getInputMap(condition);
ActionMap actionMap = getActionMap();
KeyStroke keyPressed = KeyStroke.getKeyStroke(keyCode, 0, false);
KeyStroke keyReleased = KeyStroke.getKeyStroke(keyCode, 0, true);
inputMap.put(keyPressed, keyPressed.toString());
inputMap.put(keyReleased, keyReleased.toString());
actionMap.put(keyPressed.toString(), new MoveAction(dir, false));
actionMap.put(keyReleased.toString(), new MoveAction(dir, true));
}
#Override
public Dimension getPreferredSize() {
if (isPreferredSizeSet()) {
return super.getPreferredSize();
}
return new Dimension(PREF_W, PREF_H);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
sprite.draw(g);
}
private class MoveAction extends AbstractAction {
private SpriteDirection dir;
private boolean released;
public MoveAction(SpriteDirection dir, boolean released) {
this.dir = dir;
this.released = released;
}
#Override
public void actionPerformed(ActionEvent e) {
if (released) {
sprite.setMoving(false);
} else {
sprite.setMoving(true);
sprite.setDirection(dir);
}
}
}
private class TimerListener implements ActionListener {
#Override
public void actionPerformed(ActionEvent e) {
if (sprite.isMoving()) {
sprite.tick();
}
repaint();
}
}
private static void createAndShowGui() {
Mcve3 mainPanel = new Mcve3();
JFrame frame = new JFrame("MCVE");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(mainPanel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> createAndShowGui());
}
}
class MySprite {
private static final String SPRITE_SHEET_PATH = "http://"
+ "orig12.deviantart.net/7db3/f/2010/338/3/3/"
+ "animated_sprite_sheet_32x32_by_digibody-d3479l2.gif";
private static final int MAX_MOVING_INDEX = 4;
private static final int DELTA = 4;
private SpriteDirection direction;
private Map<SpriteDirection, Image> standingImgMap = new EnumMap<>(SpriteDirection.class);
private Map<SpriteDirection, List<Image>> movingImgMap = new EnumMap<>(SpriteDirection.class);
private int x;
private int y;
private boolean moving = false;
private int movingIndex = 0;
public MySprite(SpriteDirection direction, int x, int y) throws IOException {
this.direction = direction;
this.x = x;
this.y = y;
createSprites();
}
public void draw(Graphics g) {
Image img = null;
if (!moving) {
img = standingImgMap.get(direction);
} else {
img = movingImgMap.get(direction).get(movingIndex);
}
g.drawImage(img, x, y, null);
}
private void createSprites() throws IOException {
URL spriteSheetUrl = new URL(SPRITE_SHEET_PATH);
BufferedImage img = ImageIO.read(spriteSheetUrl);
// get sub-images (sprites) from the sprite sheet
// magic numbers for getting sprites from sheet, all obtained by trial and error
int x0 = 0;
int y0 = 64;
int rW = 32;
int rH = 32;
for (int row = 0; row < 4; row++) {
SpriteDirection dir = SpriteDirection.values()[row];
List<Image> imgList = new ArrayList<>();
movingImgMap.put(dir, imgList);
int rY = y0 + row * rH;
for (int col = 0; col < 5; col++) {
int rX = x0 + col * rW;
BufferedImage subImg = img.getSubimage(rX, rY, rW, rH);
if (col == 0) {
// first image is standing
standingImgMap.put(dir, subImg);
} else {
// all others are moving
imgList.add(subImg);
}
}
}
}
public SpriteDirection getDirection() {
return direction;
}
public void setDirection(SpriteDirection direction) {
if (this.direction != direction) {
setMoving(false);
}
this.direction = direction;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public boolean isMoving() {
return moving;
}
public void setMoving(boolean moving) {
this.moving = moving;
if (!moving) {
movingIndex = 0;
}
}
public void tick() {
if (moving) {
switch (direction) {
case RIGHT:
x += DELTA;
break;
case LEFT:
x -= DELTA;
break;
case FORWARD:
y += DELTA;
break;
case AWAY:
y -= DELTA;
}
movingIndex++;
movingIndex %= MAX_MOVING_INDEX;
}
}
public int getMovingIndex() {
return movingIndex;
}
public void setMovingIndex(int movingIndex) {
this.movingIndex = movingIndex;
}
}
enum SpriteDirection {
FORWARD, LEFT, AWAY, RIGHT
}
As an alternative to KeyListener, consider using actions and key bindings, discussed here. Derived from this example, the program below moves a line left, down, up or right using either buttons or keys.
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
/**
* #see https://stackoverflow.com/questions/6991648
* #see https://stackoverflow.com/questions/6887296
* #see https://stackoverflow.com/questions/5797965
*/
public class LinePanel extends JPanel {
private MouseHandler mouseHandler = new MouseHandler();
private Point p1 = new Point(100, 100);
private Point p2 = new Point(540, 380);
private boolean drawing;
public LinePanel() {
this.setPreferredSize(new Dimension(640, 480));
this.addMouseListener(mouseHandler);
this.addMouseMotionListener(mouseHandler);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(Color.blue);
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setStroke(new BasicStroke(8,
BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL));
g.drawLine(p1.x, p1.y, p2.x, p2.y);
}
private class MouseHandler extends MouseAdapter {
#Override
public void mousePressed(MouseEvent e) {
drawing = true;
p1 = e.getPoint();
p2 = p1;
repaint();
}
#Override
public void mouseReleased(MouseEvent e) {
drawing = false;
p2 = e.getPoint();
repaint();
}
#Override
public void mouseDragged(MouseEvent e) {
if (drawing) {
p2 = e.getPoint();
repaint();
}
}
}
private class ControlPanel extends JPanel {
private static final int DELTA = 10;
public ControlPanel() {
this.add(new MoveButton("\u2190", KeyEvent.VK_LEFT, -DELTA, 0));
this.add(new MoveButton("\u2191", KeyEvent.VK_UP, 0, -DELTA));
this.add(new MoveButton("\u2192", KeyEvent.VK_RIGHT, DELTA, 0));
this.add(new MoveButton("\u2193", KeyEvent.VK_DOWN, 0, DELTA));
}
private class MoveButton extends JButton {
KeyStroke k;
int dx, dy;
public MoveButton(String name, int code, final int dx, final int dy) {
super(name);
this.k = KeyStroke.getKeyStroke(code, 0);
this.dx = dx;
this.dy = dy;
this.setAction(new AbstractAction(this.getText()) {
#Override
public void actionPerformed(ActionEvent e) {
LinePanel.this.p1.translate(dx, dy);
LinePanel.this.p2.translate(dx, dy);
LinePanel.this.repaint();
}
});
ControlPanel.this.getInputMap(
WHEN_IN_FOCUSED_WINDOW).put(k, k.toString());
ControlPanel.this.getActionMap().put(k.toString(), new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
MoveButton.this.doClick();
}
});
}
}
}
private void display() {
JFrame f = new JFrame("LinePanel");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(this);
f.add(new ControlPanel(), BorderLayout.SOUTH);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
new LinePanel().display();
}
});
}
}
But basically I just want to know how to make the image move left to right while the window is listening to a keypress
You can use a Swing Timer to animate an image:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class TimerAnimation extends JLabel implements ActionListener
{
int deltaX = 2;
int deltaY = 3;
int directionX = 1;
int directionY = 1;
public TimerAnimation(
int startX, int startY,
int deltaX, int deltaY,
int directionX, int directionY,
int delay)
{
this.deltaX = deltaX;
this.deltaY = deltaY;
this.directionX = directionX;
this.directionY = directionY;
setIcon( new ImageIcon("dukewavered.gif") );
// setIcon( new ImageIcon("copy16.gif") );
setSize( getPreferredSize() );
setLocation(startX, startY);
new javax.swing.Timer(delay, this).start();
}
public void actionPerformed(ActionEvent e)
{
Container parent = getParent();
// Determine next X position
int nextX = getLocation().x + (deltaX * directionX);
if (nextX < 0)
{
nextX = 0;
directionX *= -1;
}
if ( nextX + getSize().width > parent.getSize().width)
{
nextX = parent.getSize().width - getSize().width;
directionX *= -1;
}
// Determine next Y position
int nextY = getLocation().y + (deltaY * directionY);
if (nextY < 0)
{
nextY = 0;
directionY *= -1;
}
if ( nextY + getSize().height > parent.getSize().height)
{
nextY = parent.getSize().height - getSize().height;
directionY *= -1;
}
// Move the label
setLocation(nextX, nextY);
}
public static void main(String[] args)
{
JPanel panel = new JPanel();
JFrame frame = new JFrame();
frame.setContentPane(panel);
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
frame.getContentPane().setLayout(null);
// frame.getContentPane().add( new TimerAnimation(10, 10, 2, 3, 1, 1, 10) );
frame.getContentPane().add( new TimerAnimation(300, 100, 3, 2, -1, 1, 20) );
// frame.getContentPane().add( new TimerAnimation(0, 000, 5, 0, 1, 1, 20) );
frame.getContentPane().add( new TimerAnimation(0, 200, 5, 0, 1, 1, 80) );
frame.setSize(400, 400);
frame.setLocationRelativeTo( null );
frame.setVisible(true);
// frame.getContentPane().add( new TimerAnimation(10, 10, 2, 3, 1, 1, 10) );
// frame.getContentPane().add( new TimerAnimation(10, 10, 3, 0, 1, 1, 10) );
}
}
You can add a KeyListener to the panel and it will operate independently of the image animation.