JavaFx 8 - Scaling / zooming ScrollPane relative to mouse position - java

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.

Related

How to zoom into mouse position with correct translation

I'm trying to zoom a grid in Processing and I am having trouble with applying the correct translation such that zooming is centered around the mouse position. I have searched the web for a while but nothing I try seems to work.
The screen size is width and height and the mouse position is mouseX and mouseY.
The code I have at the moment is below, but it zooms the grid (controlled by player.zoom) from the top left corner which is not what I want. To update the translation of the grid, player has the 2d vector player.translate.
void mouseWheel(MouseEvent event) {
float zoomFactor = 200.0f;
float scroll = event.getCount();
player.zoom -= scroll * player.zoom / zoomFactor;
// player.translate.x += something;
// player.translate.y += something;
}
If you need more details to answer I can link the repo with the source code.
I have created a very simple mock-up for you which will hopefully point you in the right direction into applying this to your player.
So this little demo shows the zooming in to the centre of an ellipse whilst keeping it as the central focus.
float scale = 1;
// displacement left/right
float xPan = 720;
// displacement up/down
float yPan = 450;
boolean zoomIn = true;
void setup() {
size(1440, 900);
}
void draw() {
// allows us to zoom into the center of the screen rather than the corner
translate(width/2, height/2);
scale(scale);
translate(-xPan, -yPan);
background(200);
// draws the ellipse in the center
ellipse(width/2, height/2, 100, 100);
// does the zooming
if (zoomIn) {
scale *= 1.01;
}
}
I suggest you to copy this into a new project and then comment out specific lines to try to understand what's going on and how you can transfer this over to your own project.
The same principles still apply, I didn't do this with mouse input in the effort of making the program as simple as possible.

mapping two points on image to another one

I am trying to map two points on one image to two points on the original image so i divided the work into three main actions first scaling the n rotation then translation after everything but cant position them correctly the scaling works fine and the translation also the rotation works perfectly if i didn't scale the images only way the rotation work perfectly when i rotate around custom point but the image get distorted
Rotate rotation = new Rotate();
rotation.setPivotX(proj.s2[0]);
rotation.setPivotY(proj.s2[1]);
MainView1.getTransforms().add(rotation);
MainView1.setManaged(false);
rotation.setAngle(Angle);
here is the code without custom rotation
guidebutton.setOnMouseClicked(event->{
if (!first_rot) {
proj.f2[0]=Lball.getCenterX();
proj.f2[1]= Lball.getCenterY();
proj.f1[0]=Rball.getCenterX();
proj.f1[1]= Rball.getCenterY();
MainView.setStyle("-fx-opacity : 0.0;");
guidetext.setText("now position them on the second image and click done");
first_rot=true;
}else {
proj.s2[0]=Lball.getCenterX();
proj.s2[1]= Lball.getCenterY();
proj.s1[0]=Rball.getCenterX();
proj.s1[1]= Rball.getCenterY();
//fixing the image first then fixing the points
// fixing the image
//adjusting the scale
double f[]=tranformations.dis_vec_d(proj.f1, proj.f2);//get the distance between the two points on the first image
double s[]=tranformations.dis_vec_d(proj.s1, proj.s2);//get the distance between the two points on the secondimage
double facx=f[0]/s[0];//factor of scale in x direction
double facy=f[1]/s[1];//factor of scale in y direction
//getting the position of second image inside the window
Bounds bounds = MainView1.getBoundsInLocal();
Bounds screenBounds = MainView1.localToScreen(bounds);
double x = screenBounds.getMinX();
double y = screenBounds.getMinY();
MainView1.setScaleX(facx);
// get the new position of image after scaling to adjust the position
bounds = MainView1.getBoundsInLocal();
screenBounds = MainView1.localToScreen(bounds);
double nx = screenBounds.getMinX();
double ny = screenBounds.getMinY();
double nmx = screenBounds.getMaxX()-nx;
double nmy = screenBounds.getMaxY()-ny;
MainView1.setTranslateX(x-nx);
MainView1.setTranslateY(y-ny);
double[]orig={nmx/2,nmy/2};
//adjusting rotation
//calculating the angle between the two line to adjust the rotation
double Angle=tranformations.angle_d(proj.s1, proj.s2);
Angle-=tranformations.angle_d(proj.f1, proj.f2);
//Add the Rotate to the ImageView's Transforms
MainView1.setRotate(Angle);
MainView1.setTranslateX(MainView.getTranslateX()+proj.f2[0]-proj.s2[0]);
MainView1.setTranslateY(MainView.getTranslateY()+proj.f2[1]-proj.s2[1]);
}
});
both views and points in unmanaged group "draw" when i get every thing work it get down when i use zooming when positioning points on the second image
i use this code for zooming using mouse wheel
final double SCALE_DELTA = 1.1;
draw.setOnScroll(event->{
event.consume();
if (event.getDeltaY() == 0) {
return;
}
double scaleFactor =(event.getDeltaY() > 0)? SCALE_DELTA: 1/SCALE_DELTA;
draw.setScaleX(draw.getScaleX() * scaleFactor);
draw.setScaleY(draw.getScaleY() * scaleFactor);
});
edit to explain the question more i have these two separate images and i use the two red points on lights as to correctly position them over each other to so they can form the new image complete image
First align one of the points using a translation then scale using the aligned point's coordinates as pivot and finally use the same pivot point to perform a rotation aligning the other points.
The following example uses groups containing 2 circles each, but it should be simple enough to replace centerX/centerY with the image coordinates of the points:
#Override
public void start(Stage primaryStage) throws Exception {
// create group containing scene that remains in place
Circle target1 = new Circle(100, 200, 20, Color.RED);
Circle target2 = new Circle(150, 100, 20, Color.RED);
Group targetGroup = new Group(target1, target2);
// create group that will be transformed
Circle c1 = new Circle(30, 30, 20, Color.BLUE);
Circle c2 = new Circle(400, 400, 20, Color.BLUE);
Group g = new Group(c1, c2);
Scene scene = new Scene(new Pane(targetGroup, g), 500, 500);
// register handler for swapping between transformed/untransformed scene on button click
scene.setOnMouseClicked(new EventHandler<MouseEvent>() {
boolean transformed;
final Translate translate = new Translate();
final Scale scale = new Scale();
final Rotate rotate = new Rotate();
{
// add transforms to transformation target
g.getTransforms().addAll(rotate, scale, translate);
}
#Override
public void handle(MouseEvent event) {
if (transformed) {
// reset transforms to identity
translate.setX(0);
translate.setY(0);
scale.setX(1);
scale.setY(1);
rotate.setAngle(0);
} else {
// align c1 and target1
translate.setX(target1.getCenterX() - c1.getCenterX());
translate.setY(target1.getCenterY() - c1.getCenterY());
// scale
double scaleFactor = Math.hypot(target1.getCenterX() - target2.getCenterX(), target1.getCenterY() - target2.getCenterY())
/ Math.hypot(c1.getCenterX() - c2.getCenterX(), c1.getCenterY() - c2.getCenterY());
scale.setPivotX(target1.getCenterX());
scale.setPivotY(target1.getCenterY());
scale.setX(scaleFactor);
scale.setY(scaleFactor);
// rotate
rotate.setPivotX(target1.getCenterX());
rotate.setPivotY(target1.getCenterY());
rotate.setAngle(Math.toDegrees(Math.atan2(target2.getCenterY() - target1.getCenterY(), target2.getCenterX() - target1.getCenterX())
- Math.atan2(c2.getCenterY() - c1.getCenterY(), c2.getCenterX() - c1.getCenterX())));
}
transformed = !transformed;
}
});
primaryStage.setScene(scene);
primaryStage.show();
}
This is particularly simple using complex numbers.
An arbitrary similarity transform can be written
Z = a.z + b
where the modulus of a is the scaling factor, the argument of a is the rotation angle and b is the translation.
These coefficients are readily obtained from the known pairs of points by the usual two-points interpolation formula
Z = Z0 + (z - z0).(Z1 - Z0)/(z1 - z0)
or
a = (Z1 - Z0)/(z1 - z0)
b = Z0 - a.z0
You have the option of using a complex data type, or to retranscript the formulas in terms of real/imaginary parts.
If the square isn't rotated, then yeah, pick x and y independently.
If the square is rotated, the math gets a little trickier. Let's let the two end points of the diagonal be X and Y, represented as complex numbers.
Then look at the equation:
(Y - X)/(1 + i) x + X
When x = 0, this returns X. When x = 1 + i, it returns Y. In fact, this equation maps the unit rectangle onto the square whose diagonal's endpoints are X and Y.
So pick two random numbers 0 ≤ a, b ≤ 1, turn it into a random point a + bi on the unit rectangle, and then use the above equation to map into into a random point in the square.

Android: How to properly scale mouse/touch input?

I have an android game where the canvas is scaled to look the same on all devices using this code:
canvas.scale((float)(getWidth()/(double)WIDTH), (float)(getHeight()/(double)HEIGHT))
where WIDTH and HEIGHT are 1920 and 1080 respectively. My problem is that for all my touch collisions (i.e. a user touching a shape) is handled using Paths and Regions:
public static boolean collided(Path a, Path b) {
Region clip = new Region(0,0,3000, 3000);
Region rA = new Region();
rA.setPath(a, clip);
Region rB = new Region();
rB.setPath(b, clip);
return !rA.quickReject(rB) && rA.op(rB, Region.Op.INTERSECT);
}
Now I also have another scale method (that barely has any effect from what I can tell, but on occasion does have an influence) to scale coordinates and dimensions:
public static double scale(double x) {
return dpToPx(x)/Resources.getSystem().getDisplayMetrics().density;
}
My problem is that with all of this scaling I can't seem to get the mouse to be in the correct position. Here is the code I use to create the mouse Path that handles collision between the mouse and other shapes:
mouse.set(x, y);
mousePath.reset();
mousePath.addRect(mouse.x - 10, mouse.y - 10, mouse.x + 10, mouse.y + 10, Path.Direction.CW);
I draw mousePath to see where the mousePath is and this is the result I get (it is the box in green and where the mouse actually is in the general area of the blue circle)
This is the much more severe point, as it seems the closer I get to (0,0) the closer mousePath gets to being at where the mouse actually is.
So how do I get the mouse to be in the correct location?
After more searching it turns out this question is a duplicate of: https://stackoverflow.com/a/24895485/4188097.
The browser always returns the mouse position in untransformed coordinates. Your drawings have been done in transformed space. If you want to know where your mouse is in transformed space, you can convert untransformed mouse coordinates to transformed coordinates like this:
var mouseXTransformed = (mouseX-panX) / scaleFactor;
var mouseYTransformed = (mouseY-panY) / scaleFactor;

Pre-set position of spawned node

I'm trying to make app that spawns new draggable nodes on pretty big pane(which is child of scrollpane), but this node should be spawned in the center of the screen.
Q is: Are there any methods to pre-set X,Y coordinates of these new imageviews?
For example:
button.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent e) {
Bounds bounds = scrollPane.getBoundsInLocal();
Bounds screenBounds = scrollPane.localToScreen(bounds);
int mX = (int) screenBounds.getMinX();
int mY = (int) screenBounds.getMinY();
Rectangle2D primScreenBounds = Screen.getPrimary().getVisualBounds();
int x = (int) ((primScreenBounds.getWidth() - mX) /4);
int y = (int) ((primScreenBounds.getHeight() - mY) /4);
/*
System.out.println("X coords:" +x);
System.out.println("Y coords:" +y);
*/
pane.getChildren().addAll(new ImageView(imgvw.getImage()));
//somehow set coordinates of new ImageView
}
});
The way you'd set the position de´pends on the layout you use. However assuming you use Pane, you could use
layoutX and layoutY or
translateX and translateY
ImageView iv = new ImageView(imgvw.getImage());
iv.setLayoutX(x);
iv.setLayoutY(y);
pane.getChildren().add(iv);
Other layouts, e.g. StackPane automatically set layoutX and layoutY. In this case you could set managed to false
iv.setManaged(false);
or use the appropriate parameters for the layout type.
This might depend on the parent the ImageViews are placed in, though in general you should be able to position Nodes with Region#positionInArea() (Region is superclass of Parent). Note that this is a protected method, meaning you might want to create your own parent (e.g. by extending StackPane for example).
That being said, there are plenty of Parent Nodes, each providing their own unique behavior. Try to make sure that the desired behavior cannot be achieved using any of those before creating your own implementation. (And since you dont specify any positioning behavior, its hard to make recommendations)

Firing projectiles in a circle

So I can't seem to find an answer to this, but I am trying to fire bullets into a circle. I have a simple class for a circular path that I attach to a bullet and it reads from that class a position when given a time value. The bullet simply increments this time value, constantly updating its position to the next. This can be improved but until I get the logic down this is what I have. I know this method works because I tried it with a linear path. The problem is applying it to a circular path.
I want the bullet to circle around a point (say Point 'Center') with a given radius and speed. I want all bullets to travel at the same speed no matter the radius of the circle so a larger circle will take longer to complete than a shorter one. Currently what is happening is I have the CircularPath object giving saying x = r * cos(t) and y = r * sin (t) where t is in radians, but this is making a circle that increases in speed as the radius increases and the radius and center of this circle is completely off. The bullets are starting in the correct position, except the radius and speeds are off. I hope I am describing this adequately. I will post the code for anyone to inspect.
package io.shparki.tetris.go;
import io.shparki.tetris.util.Point2D;
import java.awt.Color;
import java.awt.Graphics2D;
public class CircularPath extends Path{
private double radius;
// Where Start is the center and end is the location of mouse
// Radius will be distance between the two
public CircularPath(Point2D start, Point2D end) {
super(start, end);
radius = normalToEnd.getLength();
color = Color.YELLOW;
}
public Point2D getPointAtTime(double time){
double px = start.getX() + radius * Math.cos(Math.toRadians(time));
double py = start.getY() - radius * Math.sin(Math.toRadians(time));
return new Point2D(px, py);
}
public double getFinalTime() { return 0; }
public CircularPath getClone() { return new CircularPath(start.getClone(), end.getClone()); }
public void update(){
super.update();
radius = normalToEnd.getLength();
}
public void render(Graphics2D g2d){
super.render(g2d);
g2d.drawLine((int)start.getX(), (int)start.getY(), (int)end.getX(), (int)end.getY());
//g2d.drawOval((int)(start.getX() - radius), (int)(start.getY() - radius), (int)radius * 2, (int)radius * 2);
}
}
x = r * cos(t/r)
y = r * sin(t/r)
The other solution is to model 2d momentum and impose a "gravitational force" toward the center point (or ellipsoidal focus, more generally) that you want the moving object to orbit around.
(The classic Space Wars game was implemented on a machine too slow to handle the trig computations in realtime, so they precomputed a 2d array each for the x and y components of the gravity field; they could then just do a table lookup based on the ship's last position and use that to update its momentum, which was then used to update its position. Slower machines forced more clever solutions.)

Categories