Calculating offset and zoom in a graph after a mouse wheel scroll - java

I am currently working on a project in Java where I need to implement a draggable and zoomable graph.
The current implementation involves a graph object having an array of predefined points to draw. And whenever the graph wants to draw a point to the screen, it asks a "graph transformer" to apply affine transformations to the point to get its location on the screen.
The graph transformer contains an X and Y offset as well as a zoom. So basically, a point P will be transformed as follows :
P_x = zoom_x * P_x + offset_x
P_y = zoom_y * P_y + offset_y
The offset is updated when the user drags the graph and the zoom is updated when the user scrolls the mouse wheel.
Everything is working just as intended. The thing is that the zoom is always applied relative to the origin (0,0) of the graph, which is normal. But what I would like to do is apply the zoom relative to the mouse position.
I've been scratching my head for quite some time now, and I am getting a bit confused between the real coordinates and the transformed coordinates.
How could I implement this feature without modifying too much my code ?
Here are my Java methods :
The method transforming points :
public Point transform(Point p) {
Point transformed = new Point();
transformed.x = Math.round(Math.round((m_zoom.x * p.x) + m_offset.x));
transformed.y = Math.round(Math.round((m_zoom.y * p.y) + m_offset.y));
return transformed;
}
The methods called when mouse events are fired :
#Override
public void mousePressed(MouseEvent e) {
m_pressed = e.getPoint();
m_lastCalculatedOffset.x = 0;
m_lastCalculatedOffset.y = 0;
}
#Override
public void mouseDragged(MouseEvent e) {
Point zoomedDragging = new Point();
zoomedDragging.x = Math.round(Math.round(e.getX() - m_pressed.x));
zoomedDragging.y = Math.round(Math.round(e.getY() - m_pressed.y));
m_offset.x += zoomedDragging.x - m_lastCalculatedOffset.x;
m_offset.y += zoomedDragging.y - m_lastCalculatedOffset.y;
m_lastCalculatedOffset.x = zoomedDragging.x;
m_lastCalculatedOffset.y = zoomedDragging.y;
}
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
m_zoom.x *= Math.pow(ZOOM_FACTOR, e.getWheelRotation());
m_zoom.y *= Math.pow(ZOOM_FACTOR, e.getWheelRotation());
}
Thanks in advance for your help :)

Ok so after taking a break and drawing the problem on paper user graph examples, I managed to find a solution :
If we call ZoomP the location of the mouse when the zoom occurs, once the zoom has been done, the following transformation has to be done to the offset :
newOffset_x = oldOffset_x + (1 - (newZoom_x / oldZoom_x)) * (ZoomP_x - oldOffset_x)
newOffset_y = oldOffset_y + (1 - (newZoom_y / oldZoom_y)) * (ZoomP_y - oldOffset_y)
So here is my updated mouseWheelMoved method for those interested :
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
Point mousePos = e.getPoint();
Point2D.Double newZoom = new Point2D.Double();
newZoom.x = m_zoom.x * Math.pow(ZOOM_FACTOR, e.getWheelRotation());
newZoom.y = m_zoom.y * Math.pow(ZOOM_FACTOR, e.getWheelRotation());
m_offset.x += Math.round(Math.round((1 - (newZoom.x / m_zoom.x)) * (mousePos.x - m_offset.x)));
m_offset.y += Math.round(Math.round((1 - (newZoom.y / m_zoom.y)) * (mousePos.y - m_offset.y)));
m_zoom = newZoom;
}

Related

Java Multi-Display Handling under Windows - Bug with scaled displays?

tl;dr
Under Windows 10, if I put my secondary display to the right of the primary one, and apply a scaling (e.g. 150%) to the secondary, then the display coordinates (as returned by the Java API) overlap instead of letting the display bounds sit side by side. In other words, if I slowly move my mouse from the left edge of the primary to the right edge of the secondary, Java's API MouseInfo.getPointerInfo().getLocation() returns an increasing X-position from 0 to 1920, then once the cursor enters the second screen, the value jumps back down to 1280 and then increases again to 2560. So the 1280-1920 range is returned twice, for different areas.
At the end of the post, I have included an (updated) demo that makes the issue obvious. Don't hesitate to try it and report back.
The long version:
This text gives (too) much context but is also meant to share the things I learned while searching on the topic.
First, why bother ? Because I am building a screen capture application in Java that requires a correct handling of multi-display configurations, including displays where Windows' scaling feature is applied.
Using the Java API (GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()), as long as the scaling is 100%, one can observe that the primary display has its top left corner at the origin (0,0), with the other displays having coordinates "next" to the main one.
The following pictures were made using the code at the end of the post.
E.g. if we have 2 full-hd displays, the main one has its top left corner is at (0,0), while...
if the secondary is positioned at its right, at the same level, its top left corner is (1920,0):
if the secondary is positioned at its left, at the same level, its top left corner is (-1920,0):
if the secondary is positioned below, aligned horizontally, its top left corner is (0,1080):
if the secondary is positioned above, aligned horizontally, its top left corner is (0,-1080):
and so on if the displays are not aligned:
or with different resolutions:
However, if the secondary display is scaled, things go awry: it seems the scaling factor is applied not only to its dimensions, but also its origin, which gets closer to (0,0).
If the secondary is on the left, it makes sense. For example, when the secondary 1920x1080 is scaled at 150%, it makes a logical 1280x720 positioned at (-1280,0):
But if the secondary is on the right, the origin is also scaled to (1280,0), getting closer to the origin and causing it to "overlap" the primary one:
In other words, if the mouse is at (1800,0) - see red dot above - I see no way of knowing if it actually is positioned on the right of the first display (at 120px from the right edge) or on the left of the secondary one (at 520px of the left edge). When moving the mouse from the primary to the secondary display in this case, the X position of the mouse "jumps back" when it reaches the border of the primary display.
The same is true for positioning a window on the screens. If I set the X-position of a dialog to 1800, I have no way to know where it will open.
After much browsing, some answers like this one indicate that the only way to query Windows scaling is by using native calls. Indeed, using JNA, one can get the physical size of the displays (although the answer seems to indicate that call should return the logical size). I.e the JNA calls ignore the scaling factor, and behaves exactly like the Java API when scaling is at 100%:
So am I missing something ?
Not knowing the scaling factor is a small issue, but not being able to tell which display the mouse is over, or not being able to position a window on the display I want looks like a real problem to me. Is it a Java Bug ?
Note: Here is the code for the app used above, run on with OpenJDK14 on Windows 10 64b. It shows a scaled down version of your display setup and mouse position as perceived by Java. It can also place and move a small dialog across the real screens if you click and drag inside the small rectangles. Credit: The UI is inspired by the WheresMyMouse code posted here.
As is, the code uses only the Java API.
If you want to compare with JNA, search for the 4 blocks marked "JNA_ONLY", uncomment them, and add the jna libs. The demo will then toggle between JNA and Java API for displaying screen bounds and mouse cursor at each right-click. The dialog positioning never uses JNA in this version.
// JNA_ONLY
//import com.sun.jna.platform.win32.User32;
//import com.sun.jna.platform.win32.WinDef;
//import com.sun.jna.platform.win32.WinUser;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
/**
* Java multi-display detection and analysis.
* UI idea based on WheresMyMouse - https://stackoverflow.com/a/21592711/13551878
*/
public class ShowDisplays {
private static boolean useJna = false;
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new JFrame("Display Configuration");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
public static class TestPane extends JPanel {
private List<Rectangle> screenBounds;
JDialog dlg;
public TestPane() {
screenBounds = getScreenBounds();
// refresh screen details every second to reflect changes in Windows Preferences in "real time"
new Timer(1000, e -> screenBounds = getScreenBounds()).start();
// Refresh mouse position at 25fps
new Timer(40, e -> repaint()).start();
MouseAdapter mouseAdapter = new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
if (e.getButton() != MouseEvent.BUTTON1) {
useJna = !useJna;
repaint();
}
}
#Override
public void mousePressed(MouseEvent e) {
System.out.println(e.getButton());
if (e.getButton() == MouseEvent.BUTTON1) {
if (!dlg.isVisible()) {
dlg.setVisible(true);
}
moveDialogTo(e.getPoint());
}
}
#Override
public void mouseDragged(MouseEvent e) {
moveDialogTo(e.getPoint());
}
private void moveDialogTo(Point mouseLocation) {
final Rectangle surroundingRectangle = getSurroundingRectangle(screenBounds);
double scaleFactor = Math.min((double) getWidth() / surroundingRectangle.width, (double) getHeight() / surroundingRectangle.height);
int xOffset = (getWidth() - (int) (surroundingRectangle.width * scaleFactor)) / 2;
int yOffset = (getHeight() - (int) (surroundingRectangle.height * scaleFactor)) / 2;
int screenX = surroundingRectangle.x + (int) ((mouseLocation.x - xOffset) / scaleFactor);
int screenY = surroundingRectangle.y + (int) ((mouseLocation.y - yOffset) / scaleFactor);
dlg.setLocation(screenX - dlg.getWidth() / 2, screenY - dlg.getHeight() / 2);
}
};
addMouseListener(mouseAdapter);
addMouseMotionListener(mouseAdapter);
// Prepare the test dialog
dlg = new JDialog();
dlg.setTitle("Here");
dlg.setSize(50, 50);
dlg.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
// Mouse position
Point mousePoint = getMouseLocation();
g2d.setColor(Color.BLACK);
g2d.fillRect(0, 0, getWidth(), getHeight());
final Rectangle surroundingRectangle = getSurroundingRectangle(screenBounds);
double scaleFactor = Math.min((double) getWidth() / surroundingRectangle.width, (double) getHeight() / surroundingRectangle.height);
int xOffset = (getWidth() - (int) (surroundingRectangle.width * scaleFactor)) / 2;
int yOffset = (getHeight() - (int) (surroundingRectangle.height * scaleFactor)) / 2;
g2d.setColor(Color.BLUE);
g2d.fillRect(xOffset, yOffset, (int) (surroundingRectangle.width * scaleFactor), (int) (surroundingRectangle.height * scaleFactor));
Font defaultFont = g2d.getFont();
for (int screenIndex = 0; screenIndex < screenBounds.size(); screenIndex++) {
Rectangle screen = screenBounds.get(screenIndex);
Rectangle scaledRectangle = new Rectangle(
xOffset + (int) ((screen.x - surroundingRectangle.x) * scaleFactor),
yOffset + (int) ((screen.y - surroundingRectangle.y) * scaleFactor),
(int) (screen.width * scaleFactor),
(int) (screen.height * scaleFactor));
// System.out.println(screen + " x " + scaleFactor + " -> " + scaledRectangle);
g2d.setColor(Color.DARK_GRAY);
g2d.fill(scaledRectangle);
g2d.setColor(Color.GRAY);
g2d.draw(scaledRectangle);
// Screen text details
g2d.setColor(Color.WHITE);
// Display number
final Font largeFont = new Font(defaultFont.getName(), defaultFont.getStyle(), (int) (screen.height * scaleFactor) / 2);
g2d.setFont(largeFont);
String label = String.valueOf(screenIndex + 1);
FontRenderContext frc = g2d.getFontRenderContext();
TextLayout layout = new TextLayout(label, largeFont, frc);
Rectangle2D bounds = layout.getBounds();
g2d.setColor(Color.WHITE);
g2d.drawString(
label,
(int) (scaledRectangle.x + (scaledRectangle.width - bounds.getWidth()) / 2),
(int) (scaledRectangle.y + (scaledRectangle.height + bounds.getHeight()) / 2)
);
// Resolution + corner
final Font smallFont = new Font(defaultFont.getName(), defaultFont.getStyle(), (int) (screen.height * scaleFactor) / 10);
g2d.setFont(smallFont);
// Resolution
String resolution = screen.width + "x" + screen.height;
layout = new TextLayout(resolution, smallFont, frc);
bounds = layout.getBounds();
g2d.drawString(
resolution,
(int) (scaledRectangle.x + (scaledRectangle.width - bounds.getWidth()) / 2),
(int) (scaledRectangle.y + scaledRectangle.height - bounds.getHeight())
);
// Corner
String corner = "(" + screen.x + "," + screen.y + ")";
g2d.drawString(
corner,
scaledRectangle.x,
(int) (scaledRectangle.y + bounds.getHeight() * 1.5)
);
}
g2d.setFont(defaultFont);
FontMetrics fm = g2d.getFontMetrics();
if (mousePoint != null) {
g2d.fillOval(xOffset + (int) ((mousePoint.x - surroundingRectangle.x) * scaleFactor) - 2,
yOffset + (int) ((mousePoint.y - surroundingRectangle.y) * scaleFactor) - 2,
4,
4
);
g2d.drawString("Mouse pointer is at (" + mousePoint.x + "," + mousePoint.y + ")", 4, fm.getHeight());
}
g2d.drawString("Click and drag in this area to move a dialog on the actual screens", 4, fm.getHeight() * 2);
// JNA_ONLY
// g2d.drawString("Now using " + (useJna ? "JNA" : "Java API") + ". Right-click to toggle", 4, fm.getHeight() * 3);
g2d.dispose();
}
}
public static Rectangle getSurroundingRectangle(List<Rectangle> screenRectangles) {
Rectangle surroundingBounds = null;
for (Rectangle screenBound : screenRectangles) {
if (surroundingBounds == null) {
surroundingBounds = new Rectangle(screenRectangles.get(0));
}
else {
surroundingBounds.add(screenBound);
}
}
return surroundingBounds;
}
private static Point getMouseLocation() {
// JNA_ONLY
// if (useJna) {
// final WinDef.POINT point = new WinDef.POINT();
// if (User32.INSTANCE.GetCursorPos(point)) {
// return new Point(point.x, point.y);
// }
// else {
// return null;
// }
// }
return MouseInfo.getPointerInfo().getLocation();
}
public static List<Rectangle> getScreenBounds() {
List<Rectangle> screenBounds;
// JNA_ONLY
// if (useJna) {
// screenBounds = new ArrayList<>();
// // Enumerate all monitors, and call a code block for each of them
// // See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaymonitors
// // See http://www.pinvoke.net/default.aspx/user32/EnumDisplayMonitors.html
// User32.INSTANCE.EnumDisplayMonitors(
// null, // => the virtual screen that encompasses all the displays on the desktop.
// null, // => don't clip the region
// (hmonitor, hdc, rect, lparam) -> {
// // For each found monitor, get more information
// // See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmonitorinfoa
// // See http://www.pinvoke.net/default.aspx/user32/GetMonitorInfo.html
// WinUser.MONITORINFOEX monitorInfoEx = new WinUser.MONITORINFOEX();
// User32.INSTANCE.GetMonitorInfo(hmonitor, monitorInfoEx);
// // Retrieve its coordinates
// final WinDef.RECT rcMonitor = monitorInfoEx.rcMonitor;
// // And convert them to a Java rectangle, to be added to the list of monitors
// screenBounds.add(new Rectangle(rcMonitor.left, rcMonitor.top, rcMonitor.right - rcMonitor.left, rcMonitor.bottom - rcMonitor.top));
// // Then return "true" to continue enumeration
// return 1;
// },
// null // => No additional info to pass as lparam to the callback
// );
// return screenBounds;
// }
GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[] screenDevices = graphicsEnvironment.getScreenDevices();
screenBounds = new ArrayList<>(screenDevices.length);
for (GraphicsDevice screenDevice : screenDevices) {
GraphicsConfiguration configuration = screenDevice.getDefaultConfiguration();
screenBounds.add(configuration.getBounds());
}
return screenBounds;
}
}
This looks like you've run into a manifestation of bug JDK-8211999:
In a multi-monitor setting involving one HiDPI screen placed to the right of one regular monitor, on Windows 10, the bounds returned by GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()[x].getDefaultConfiguration().getBounds() are overlapping. This causes various secondary bugs...
Comments note that:
The same bug exists on Linux as well, macOS is not affected.
There does not seem to be a simple pure Java workaround.
A fix has been proposed which works for Windows, by not even trying to do the coordinate math in Java, and delegating the solution to native code.
Since it appears that using the JNA (native) implementation appears to work, this seems the best approach for JDK versions 9 to 15. The bug was fixed in JDK16.
According to the bug report, it affects JDK 9+, so it is possible that reverting to JDK 8 may fix the issue, although I saw conflicting accounts on that.

JavaFX: Polyline scaling / zooming

this is for homework.
We have to create a JavaFX application, complying to the MVP principle, that shows a static sine-wave, with sliders to control the properties of said sine-wave.
These sliders are the amplitude, frequency, phase, and zoom. They're bound, through the presenter, to properties in my model that make up the sine-wave. These then have listeners to update the model on changes.
For drawing my sine-wave, I chose a polyline and I calculate the X and Y coordinates for each point to a observable list in my model:
for (double x = -360; x < 360; x++) {
data.add(x);
data.add(Math.sin(frequency.doubleValue() * x + phase.doubleValue()) * amplitude.doubleValue());
}
Then I reach this dataset to my view through the presenter where I give each point to my polyline:
public void setPoints(ObservableList<Double> list) {
functionLine.getPoints().clear();
functionLine.getPoints().addAll(list);
double temp;
for(int i = 0;i<functionLine.getPoints().size();i++) {
//separate x from y coordinates
if (i % 2 == 0) {
temp = functionLine.getPoints().get(i);
functionLine.getPoints().set(i, temp + (display.getWidth() / 2)); // + displaywidth/2 to get it to the center of the graph
} else {
temp = functionLine.getPoints().get(i);
functionLine.getPoints().set(i, temp + ((display.getHeight() / 2))); //same as above
}
}
}
This also doesn't perform very well because of the for-loop and the interface is laggy, but that's not why I am here.
This is what is currently looks like. The polyline and graph (two lines) are located in its own pane:
Now I have tried to also add zoom to this without increasing the width of the actual line, but I can't figure out how to properly scale around the center of my graph. Obviously I have to transform the coordinates of each point, but I don't know how. I have tried several things but it doesn't achieve what I want.
Feels like something I should be able to do on my own, but I can't apparently.
Any help would be appreciated.

Libgdx/Java: After converting GPS to pixelcoordinates, route is rotated 90°

I had written before about implementing a map in my libgdx project.
I did that using a snapshot of said google map, importing the GPS bounds of the snapshot, the route-latlong values, a locationservice (via interface) and the snapshot as Gdx.files.local string.
Hopefully, the last issue I have right now is that the route is rotated about 45 degrees clockwise. Otherwise my 'enemies' walk a perfect path. I already figured out that I had to 'flip' my y-axis; before that it was rotated AND flipped upside down.
I was hoping someone here with more experience might have dealt with something similar before and has some advice :)
This is basically the code that creates a Waypoint array, after converting the GPS coordinates to pixel-coordinates that correspond to the gps-bounds of the map-snapshot (bottom-left-corner and upper-right-corner see here, as well as the width and height of the map-texture.
private void convertPathToScreen(double[] gpsRoute){
for(int i = 0; i<gpsRoute.length; i++){
if(i % 2 != 0) {
screenRouteCoordinates[i] =
x_GpsToScreenConversion(gpsRouteCoordinates[i]);
}
else{
screenRouteCoordinates[i] =
y_GpsToScreenConversion(gpsRouteCoordinates[i]);
}
}
}
public int x_GpsToScreenConversion(double x_pointInGpsCoords){
double percentage = 1 - Math.abs((x_pointInGpsCoords - x_GpsMin) /
(x_GpsMax - x_GpsMin));
return (int)((percentage* Math.abs(mapWidth - mapOrigin)) + mapOrigin);
}
public int y_GpsToScreenConversion(double y_pointInGpsCoords){
double percentage = (y_pointInGpsCoords - y_GpsMin) / (y_GpsMax - y_GpsMin);
return (int)((percentage* Math.abs(mapHeight - mapOrigin)) + mapOrigin);
}
Edit: Now that I think of it, the error might be in my pathfinding code, although I tested it before moving my project forward and it worked solidly for all values I put in. Anyway, for completness...es sake
private void calculatePathing(){
angle = (float) (Math.atan2(waypointsToGoal[waypoint].y - getY(), waypointsToGoal[waypoint].x - getX()));
velocity.set((float) Math.cos(angle) * speed, (float) Math.sin(angle) * speed);
}
So the question is basically: How do I fix the 90° clockwise rotation that buggers up my game? Can I rotate the coordinates of the array around the center of the map (where all enemies walk to) or is there a mistake in the conversion-code here?
Solution: (Patchwork, but it gets the job done!)
I simply rotated my waypoints by the degree I needed around the destination-point. It doesn't solve the underlying issue, but it solves the symptom.
private void createWaypointArray(){
//formula requires radians
double angle = Math.toRadians(90);
double current_x;
double current_y;
// waypointCache.size()-1 gets me the last waypoint, the destination around which I rotate
double center_x = waypointCache.get(waypointCache.size()-1).x;
double center_y= waypointCache.get(waypointCache.size()-1).y;
// Loop through Vector2 Array, rotate the points around destination and save them
for(int i = 0; i < waypointCache.size()-1; i++){
current_x = waypointCache.get(i).x;
current_y = waypointCache.get(i).y;
waypointCache.get(i).x = (float)((current_x-center_x) * Math.cos(angle) - (current_y-center_y) * Math.sin(angle) + center_x);
waypointCache.get(i).y = (float)((current_x-center_x) * Math.sin(angle) + (current_y-center_y) * Math.cos(angle) + center_y);
// this does work, but also translates the points because it rotates around the
// worldaxis, usable when points lie on normal kartesian axis I guess
// waypointCache.get(i).rotate(90);
}
this.wayPointArray = waypointCache.toArray(new Vector2[waypointCache.size()]);
}

JavaFx 8 - Scaling / zooming ScrollPane relative to mouse position

I need to zoom in / out on a scroll pane, relative to the mouse position.
I currently achieve the zooming functionality by wrapping my content in a Group, and scaling the group itself. I create a new Scale object with a custom pivot. (Pivot is set to the mouse position)
This works perfectly for where the Group's initial scale is 1.0, however scaling afterwards does not scale in the correct direction - I believe this is because the relative mouse position changes when the Group has been scaled.
My code:
#Override
public void initialize(URL location, ResourceBundle resources) {
Delta initial_mouse_pos = new Delta();
anchorpane.setOnScrollStarted(event -> {
initial_mouse_pos.x = event.getX();
initial_mouse_pos.y = event.getY();
});
anchorpane.setOnScroll(event -> {
double zoom_fac = 1.05;
double delta_y = event.getDeltaY();
if(delta_y < 0) {
zoom_fac = 2.0 - zoom_fac;
}
Scale newScale = new Scale();
newScale.setPivotX(initial_mouse_pos.x);
newScale.setPivotY(initial_mouse_pos.y);
newScale.setX( content_group.getScaleX() * zoom_fac );
newScale.setY( content_group.getScaleY() * zoom_fac );
content_group.getTransforms().add(newScale);
event.consume();
});
}
private class Delta { double x, y; }
How do I get the correct mouse position at different levels of scaling? Is there a completely different way to zooming the ScrollPane that is easier?
This is a scalable, pannable JavaFX ScrollPane :
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
public class ZoomableScrollPane extends ScrollPane {
private double scaleValue = 0.7;
private double zoomIntensity = 0.02;
private Node target;
private Node zoomNode;
public ZoomableScrollPane(Node target) {
super();
this.target = target;
this.zoomNode = new Group(target);
setContent(outerNode(zoomNode));
setPannable(true);
setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
setFitToHeight(true); //center
setFitToWidth(true); //center
updateScale();
}
private Node outerNode(Node node) {
Node outerNode = centeredNode(node);
outerNode.setOnScroll(e -> {
e.consume();
onScroll(e.getTextDeltaY(), new Point2D(e.getX(), e.getY()));
});
return outerNode;
}
private Node centeredNode(Node node) {
VBox vBox = new VBox(node);
vBox.setAlignment(Pos.CENTER);
return vBox;
}
private void updateScale() {
target.setScaleX(scaleValue);
target.setScaleY(scaleValue);
}
private void onScroll(double wheelDelta, Point2D mousePoint) {
double zoomFactor = Math.exp(wheelDelta * zoomIntensity);
Bounds innerBounds = zoomNode.getLayoutBounds();
Bounds viewportBounds = getViewportBounds();
// calculate pixel offsets from [0, 1] range
double valX = this.getHvalue() * (innerBounds.getWidth() - viewportBounds.getWidth());
double valY = this.getVvalue() * (innerBounds.getHeight() - viewportBounds.getHeight());
scaleValue = scaleValue * zoomFactor;
updateScale();
this.layout(); // refresh ScrollPane scroll positions & target bounds
// convert target coordinates to zoomTarget coordinates
Point2D posInZoomTarget = target.parentToLocal(zoomNode.parentToLocal(mousePoint));
// calculate adjustment of scroll position (pixels)
Point2D adjustment = target.getLocalToParentTransform().deltaTransform(posInZoomTarget.multiply(zoomFactor - 1));
// convert back to [0, 1] range
// (too large/small values are automatically corrected by ScrollPane)
Bounds updatedInnerBounds = zoomNode.getBoundsInLocal();
this.setHvalue((valX + adjustment.getX()) / (updatedInnerBounds.getWidth() - viewportBounds.getWidth()));
this.setVvalue((valY + adjustment.getY()) / (updatedInnerBounds.getHeight() - viewportBounds.getHeight()));
}
}
Did you try to remove the setOnScrollStarted-event and move its content to the setOnScroll-event?
Doing so reduces the need of your extra Delta-class and the computations of your mouse-positions are always on par with the current zoom factor.
I implemented the same thing and it works the way you are describing it.
Somehow like this:
#Override
public void initialize(URL location, ResourceBundle resources) {
anchorpane.setOnScroll(event -> {
double zoom_fac = 1.05;
if(delta_y < 0) {
zoom_fac = 2.0 - zoom_fac;
}
Scale newScale = new Scale();
newScale.setPivotX(event.getX);
newScale.setPivotY(event.getY);
newScale.setX( content_group.getScaleX() * zoom_fac );
newScale.setY( content_group.getScaleY() * zoom_fac );
content_group.getTransforms().add(newScale);
event.consume();
});
}
I believe this is a duplicate of this question which involves the same concepts at work. If you don't really care if it zooms relative to your mouse and just prefer it zoom in the center look at this question. If you need any more help comment below.
Assuming you want to have the following zoom behavior:
When the mouse wheel is pushed forward/backward the object under the cursor will be scaled up/down and the area under the cursor is now centered within the zooming area.
Eg. pushing the wheel forward while pointing at a place left from the center of the zooming area results in a 'up-scale and move right' action.
The scaling thing is as simple as you have already done so far.
The tricky part is the move action. There are some problem you have to consider within your calculations:
You have to calculate the difference from the center and the
position of the cursor. You can calculate this value by subtracting
the center point (cp) from the mouse position (mp).
If your zoom level is 1 and you point 50px left from the center you want to move your object 50px to the right, because 1px of your screen corresponds to one 1px of your object (picture). But if you doubled the size of your object, than 2 screen pixel are equal to on object pixel. You have to consider this when moving the object, because the translating part is always done before the scaling part. In other words you are moving your object in original size and the scaling is only the second independent step.
How is the scaling done? In JavaFX you simply set some scale-properties and JavaFX does the rest for you. But what does it exactly? The important thing to know is, where the fixed point is while zooming the object. If you scale an object there will be one point which stays fixed at its position while all other points are moving towards this point or moving way from it.
As the documentation of JavaFX says the center of the zoomed object will be the fixed point.
Defines the factor by which coordinates are scaled about the center of
the object along the X axis of this Node.
That means you have to ensure that your visual center point is equal to the one JavaFX uses while scaling you object. You can achieve this if you wrap your object within a container. Now zoom the container instead of the object and position the object within the container to fit your needs.
I hope this helps. If you need more help please offer a short working example project.

How can I clear the screen in openGL using Java

I don't understand how I can simply clear the screen in Java while using OpenGL. I have searched all over the internet, there is like no real good resource for OpenGL information. Basically I just want to clear the screen and re-draw a circle. Instead my code decides that it isn't going to clear the screen ever, and it most definitely isn't going to draw anything else.. I want it to clear the screen when I press "e", and then draw a new circle. I have two java files.. I will only post relevant code for the sake of any user's who can help me - but will post more code if needed.
In the beginning of my JOGLEventListener.java file I'm also declaring a global var
// Test
GLAutoDrawable test = null;
JOGLEventListener.java
#Override
public void display(GLAutoDrawable gLDrawable)
{
// Set a global variable to hold the gLDrawable
// May not need this?
test = gLDrawable;
GL2 gl = gLDrawable.getGL().getGL2();
gl.glClearColor(backrgb[0], 0, 1, 1);
gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
backrgb[0]+=0.0005;
if (backrgb[0]> 1) backrgb[0] = 0;
// =============================================
// Draw my circle here
//
// =============================================
// =============================================
System.out.println("Drawing Circle..");
drawCircle(5.0f, 5.0f, 10.0f);
}
// Draw Circle
void drawCircle(float x, float y, float radius)
{
System.out.println("IN DRAWCIRCLE");
int i;
GL2 gl = test.getGL().getGL2();
int lineAmount = 100; //# of triangles used to draw circle
final
//GLfloat radius = 0.8f; //radius
float twicePi = (float) (2.0f * Math.PI);
gl.glBegin(gl.GL_LINE_LOOP);
for(i = 0; i <= lineAmount;i++) {
gl.glVertex2f(
x + (radius * (float)Math.cos(i * twicePi / lineAmount)),
y + (radius* (float)Math.sin(i * twicePi / lineAmount))
);
}
gl.glEnd();
}
#Override
public void keyTyped(KeyEvent e)
{
char key= e.getKeyChar();
System.out.printf("Key typed: %c\n", key);
GL2 gl = test.getGL().getGL2();
if(key == 'e')
{
// WHY ISNT THIS WORKING
// CLEAR THE SCREEN AND DRAW ME A NEW CIRCLE
gl.glClear( gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT );
gl.glLoadIdentity();
//test
float x = 100.0f;
float y = 100.0f;
float twicePi = (float) (2.0f * Math.PI);
float radius = 100f;
System.out.println("Draw Another Circle...");
gl.glBegin(gl.GL_LINE_LOOP);
for(int i = 0; i <= 360;i++)
{
gl.glVertex2f(
x + (radius * (float)Math.cos(i * twicePi / 360)),
y + (radius* (float)Math.sin(i * twicePi / 360))
);
}
gl.glEnd();
}
1) That's deprecated OpenGL, don't use it
2) Don't save the gl object to one global value, always get it from the drawable or the GLContext
3) Use a shader program to render and a vertex buffer to hold the vertices position. But first, I'd suggest you to start a tutorial to learn the basic of OpenGL. Or if you want to get something working asap, clone this hello triangle of mine and start experiment on that
The problem is apparently that you don't swap the front and back buffers.
I'm not familiar with the OpenGL bindings for Java, but I guess that the library already does that for you after it calls the display() function. It doesn't do that after keyTyped().
The way you are supposed to do this is to always draw the scene from scratch inside the display() function based on some internal state. Then in keyTyped() you shall modify that internal state and invalidate the window, which will cause the display() to be called again and redraw the scene properly.
EDIT: Calling display() yourself won't be enough. I can't find how to invalidate the window in Java (in C this would be so much easier). As a dirty hack you can try calling temp.swapBuffers() manually in display, setting setAutoSwapBufferMode(false) and calling display from keyTyped().

Categories