Zoom relative to mouse position

A

Amir Kouchekinia

Hi,

I am trying to figure out how to zoom relative to the mouse pointer
position. Below, please find my sample code.

When I initially position the mouse pointer on one corner of the red
rectangle and use the mouse wheel, I am able to zoom in and out as
expected. As soon as I move the pointer position, let's say to another
corner of the red rectangle, and try to zoom in or out, my position
relative to the rectangle shifts around.

I think I may be missing a transform or two. In another attempt, in the
mouseWheelMoved method, I inverted tx from the previous iteration and
transformed the mouse position p with the inverted transform before
recalculating the new transform; but the behavior got more erratic.


Any help is greatly appreciated. What am I missing here?

Thanks.

Here is my code:

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;

@SuppressWarnings("serial")
public class ZoomDemo extends JPanel {

AffineTransform tx = new AffineTransform();

Rectangle2D.Double rect = new Rectangle2D.Double(100, 100, 20, 30);

public ZoomDemo() {
this.addMouseWheelListener(new ZoomHandler());
}

@Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2 = (Graphics2D) g;

Path2D.Double path;
g2.setColor(Color.RED);
path = new Path2D.Double(rect, tx);
g2.draw(path);
}

private class ZoomHandler implements MouseWheelListener {

Point oldPoint = null;

double scale = 1.0;

public void mouseWheelMoved(MouseWheelEvent e) {
if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {

scale += (.1 * e.getWheelRotation());
scale = Math.max(0.1, scale);
Point p = e.getPoint();

tx = AffineTransform.getTranslateInstance(p.getX(), p.getY());
tx.scale(scale, scale);
tx.translate(-p.getX(), -p.getY());

ZoomDemo.this.revalidate();
ZoomDemo.this.repaint();
}
}
}

public static void main(String[] args) {

JFrame f = new JFrame("ZoomDemo");
ZoomDemo zoomDemo = new ZoomDemo();
JScrollPane sp = new JScrollPane(zoomDemo);
f.getContentPane().add(sp);
f.setSize(500, 500);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
 
J

John B. Matthews

Amir Kouchekinia said:
Hi,

I am trying to figure out how to zoom relative to the mouse pointer
position. Below, please find my sample code.

When I initially position the mouse pointer on one corner of the red
rectangle and use the mouse wheel, I am able to zoom in and out as
expected. As soon as I move the pointer position, let's say to another
corner of the red rectangle, and try to zoom in or out, my position
relative to the rectangle shifts around.

I think I may be missing a transform or two. In another attempt, in the
mouseWheelMoved method, I inverted tx from the previous iteration and
transformed the mouse position p with the inverted transform before
recalculating the new transform; but the behavior got more erratic.


Any help is greatly appreciated. What am I missing here?

You want to transform a Shape's coordinates, but the normal
concatenation of transformations is arranged for a different goal.
Recalling that matrix multiplication is _not_ commutative, if you want
to scale then translate, you have to apply then in reverse order. See
concatenate() and preConcatenate(), as well as the scaled arrow thread
in this group.

Is this the effect you wanted? [Path2D replaced by Shape for 5.0 users.]

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Shape;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

@SuppressWarnings("serial")
public class ZoomDemo extends JPanel {

AffineTransform tx = new AffineTransform();

Rectangle2D.Double rect = new Rectangle2D.Double(-15, -30, 30, 60);

public ZoomDemo() {
this.addMouseWheelListener(new ZoomHandler());
}

@Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2 = (Graphics2D) g;
g2.setColor(Color.RED);
Shape shape = tx.createTransformedShape(rect);
g2.draw(shape);
}

private class ZoomHandler implements MouseWheelListener {

double scale = 1.0;

public void mouseWheelMoved(MouseWheelEvent e) {
if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {

scale += (.1 * e.getWheelRotation());
scale = Math.max(0.1, scale);
Point p = e.getPoint();

tx.setToIdentity();
tx.translate(p.getX(), p.getY());
tx.scale(scale, scale);

ZoomDemo.this.revalidate();
ZoomDemo.this.repaint();
}
}
}

public static void main(String[] args) {

JFrame f = new JFrame("ZoomDemo");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ZoomDemo zoomDemo = new ZoomDemo();
f.getContentPane().add(zoomDemo);
f.setSize(500, 500);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}

John
 
A

Amir Kouchekinia

Hi John,

Thank you very much for your reply.

I have tried your code, and the behavior is not what I am looking for.

The red rectangle in my code was located at 100,100 point in the initial
view (Scale 1.0, No Translate). I am not sure why you have centered the
rectangle to the 0,0 point. Also, where ever I try to zoom, the red
rectangle ends up being centered around that point.

This is the behavior I want to see: When I place the mouse pointer over
the lower-left corner of the rectangle and zoom, I want the lower-left
corner of the rectangle to remain at the original mouse pointer location
relative to the screen. Now if I move the mouse pointer to the
upper-left corner of the rectangle and zoom, I want the upper-left
corner of the rectangle to remain at the new mouse pointer location
relative to the screen. If there where other shapes on the canvas, I'd
expect similar behavior when the mouse pointer is located over one of
those other shapes.

I have found an example of this behavior on my Mac OSX 10.5 desktop:
If I hold the 'control' key and use the mouse wheel to zoom in, the
point in the object displayed under the mouse pointer stays under the
mouse pointer relative to the screen.

I have looked at the Arrow thread in this group, but have not been able
to extract the answer I am looking for.

Thanks again.

Amir


Amir Kouchekinia said:
Hi,

I am trying to figure out how to zoom relative to the mouse pointer
position. Below, please find my sample code.

When I initially position the mouse pointer on one corner of the red
rectangle and use the mouse wheel, I am able to zoom in and out as
expected. As soon as I move the pointer position, let's say to another
corner of the red rectangle, and try to zoom in or out, my position
relative to the rectangle shifts around.

I think I may be missing a transform or two. In another attempt, in the
mouseWheelMoved method, I inverted tx from the previous iteration and
transformed the mouse position p with the inverted transform before
recalculating the new transform; but the behavior got more erratic.


Any help is greatly appreciated. What am I missing here?

You want to transform a Shape's coordinates, but the normal
concatenation of transformations is arranged for a different goal.
Recalling that matrix multiplication is _not_ commutative, if you want
to scale then translate, you have to apply then in reverse order. See
concatenate() and preConcatenate(), as well as the scaled arrow thread
in this group.

Is this the effect you wanted? [Path2D replaced by Shape for 5.0 users.]

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Shape;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

@SuppressWarnings("serial")
public class ZoomDemo extends JPanel {

AffineTransform tx = new AffineTransform();

Rectangle2D.Double rect = new Rectangle2D.Double(-15, -30, 30, 60);

public ZoomDemo() {
this.addMouseWheelListener(new ZoomHandler());
}

@Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2 = (Graphics2D) g;
g2.setColor(Color.RED);
Shape shape = tx.createTransformedShape(rect);
g2.draw(shape);
}

private class ZoomHandler implements MouseWheelListener {

double scale = 1.0;

public void mouseWheelMoved(MouseWheelEvent e) {
if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {

scale += (.1 * e.getWheelRotation());
scale = Math.max(0.1, scale);
Point p = e.getPoint();

tx.setToIdentity();
tx.translate(p.getX(), p.getY());
tx.scale(scale, scale);

ZoomDemo.this.revalidate();
ZoomDemo.this.repaint();
}
}
}

public static void main(String[] args) {

JFrame f = new JFrame("ZoomDemo");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ZoomDemo zoomDemo = new ZoomDemo();
f.getContentPane().add(zoomDemo);
f.setSize(500, 500);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}

John
 
J

John B. Matthews

Amir Kouchekinia said:
Hi John,

Thank you very much for your reply.

I have tried your code, and the behavior is not what I am looking for.

The red rectangle in my code was located at 100,100 point in the initial
view (Scale 1.0, No Translate). I am not sure why you have centered the
rectangle to the 0,0 point. Also, where ever I try to zoom, the red
rectangle ends up being centered around that point.

Correct. The rectangle starts out centered on the origin of a Cartesian
plane (user space). As the graphics environment's default transformation
matrix is unchanged, the rectangle is rendered upright on the screen
(device space) and centered on the origin (left, upper corner).
Translating it to the current mouse point leaves it centered on the
mouse point, scaled according to the mouse wheel. Try altering the
original rectangle from (-15, -30, 30, 60) to (-15, -5, 30, 60) to see
how the rectangle remains upright (due to the g2's default transform)
and how it is positioned (due to the tx transform).
This is the behavior I want to see: When I place the mouse pointer over
the lower-left corner of the rectangle and zoom, I want the lower-left
corner of the rectangle to remain at the original mouse pointer location
relative to the screen. Now if I move the mouse pointer to the
upper-left corner of the rectangle and zoom, I want the upper-left
corner of the rectangle to remain at the new mouse pointer location
relative to the screen. If there where other shapes on the canvas, I'd
expect similar behavior when the mouse pointer is located over one of
those other shapes.

Only the shape under the mouse changes, and the other shapes remain
unaffected? You'll have to implement MouseMotionListener, determine
which object includes the mouse point, and scale/translate the
individual rectangle relative to the current mouse position.
I have found an example of this behavior on my Mac OSX 10.5 desktop:
If I hold the 'control' key and use the mouse wheel to zoom in, the
point in the object displayed under the mouse pointer stays under the
mouse pointer relative to the screen.

In this case, the entire Desktop zooms (with the wheel) and pans (with
the mouse). Let's start with the zoom. Such an effect is most easily
achieved by concatenating transformations with the g2's default
transform. Of course, this zooms the entire view, not just a single
object as you suggest above. Compare the order of concatenation in
following example to the previous one. The rotate is superfluous, but
interesting to alter.

Panning is left as an exercise with MouseMotionListener. Hint: Instead
of translating to the center of the window (as shown below), you'd
translate in a direction determined by the difference between successive
mouse positions.

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Shape;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

@SuppressWarnings("serial")
public class ViewZoom extends JPanel {

private static final int SIZE = 500;
private double scale = 2d;
Rectangle2D.Double rect1 = new Rectangle2D.Double(-25, -35, 20, 30);
Rectangle2D.Double rect2 = new Rectangle2D.Double(5, 5, 30, 50);
Rectangle2D.Double rect3 = new Rectangle2D.Double(-15, -15, 30, 30);

public ViewZoom() {
this.addMouseWheelListener(new ZoomHandler());
}

@Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2 = (Graphics2D) g;
g2.setColor(Color.BLACK);
g2.drawLine(0, SIZE / 2, SIZE, SIZE / 2);
g2.drawLine(SIZE / 2, 0, SIZE / 2, SIZE);
AffineTransform saveAT = g2.getTransform();
g2.translate(SIZE / 2, SIZE / 2);
g2.rotate(Math.PI / 4);
g2.scale(scale, scale);
g2.setColor(Color.RED);
g2.draw(rect1);
g2.draw(rect2);
g2.setColor(Color.BLUE);
g2.draw(rect3);
g2.setTransform(saveAT);
}

private class ZoomHandler implements MouseWheelListener {

public void mouseWheelMoved(MouseWheelEvent e) {
if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {

scale += (.1 * e.getWheelRotation());
scale = Math.max(0.1, scale);
scale = Math.min(scale, 10);

ViewZoom.this.revalidate();
ViewZoom.this.repaint();
}
}
}

public static void main(String[] args) {

JFrame f = new JFrame("ViewZoom");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ViewZoom viewZoom = new ViewZoom();
f.getContentPane().add(viewZoom);
f.setSize(SIZE, SIZE);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}

John
 
J

John B. Matthews

"John B. Matthews said:
Correct. The rectangle starts out centered on the origin of a Cartesian
plane (user space). As the graphics environment's default transformation
matrix is unchanged, the rectangle is rendered upright on the screen
(device space) and centered on the origin (left, upper corner).
Translating it to the current mouse point leaves it centered on the
mouse point, scaled according to the mouse wheel. Try altering the
original rectangle from (-15, -30, 30, 60) to (-15, -5, 30, 60) to see
how the rectangle remains upright (due to the g2's default transform)
and how it is positioned (due to the tx transform).
[...]

Oops, not Cartesian. Rather, it's the vertical inverse, with ordinates
increasing top-to-bottom.

John
 
A

Amir Kouchekinia

I've finally figured this out. Here's my code if you are interested:

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

@SuppressWarnings("serial")
public class ZoomDemo extends JPanel implements Runnable {

AffineTransform tx = new AffineTransform();

Rectangle2D.Double rect1 = new Rectangle2D.Double(100, 100, 30, 60);
Rectangle2D.Double rect2 = new Rectangle2D.Double(150, 250, 60, 40);

public ZoomDemo() {
this.addMouseWheelListener(new ZoomHandler());
}

@Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2 = (Graphics2D) g;
g2.setColor(Color.RED);
g2.draw(tx.createTransformedShape(rect1));
g2.setColor(Color.BLUE);
g2.draw(tx.createTransformedShape(rect2));
}

private class ZoomHandler implements MouseWheelListener {

double scale = 1.0;

public void mouseWheelMoved(MouseWheelEvent e) {
if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {

Point2D p1 = e.getPoint();
Point2D p2 = null;
try {
p2 = tx.inverseTransform(p1, null);
} catch (NoninvertibleTransformException ex) {
// should not get here
ex.printStackTrace();
return;
}

scale -= (0.1 * e.getWheelRotation());
scale = Math.max(0.1, scale);

tx.setToIdentity();
tx.translate(p1.getX(), p1.getY());
tx.scale(scale, scale);
tx.translate(-p2.getX(), -p2.getY());

ZoomDemo.this.revalidate();
ZoomDemo.this.repaint();
}
}
}

public void run() {
JFrame f = new JFrame("Zoom Demo");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().add(this);
f.setSize(600, 600);
f.setLocationRelativeTo(null);
f.setVisible(true);
}

public static void main(String[] args) {
SwingUtilities.invokeLater(new ZoomDemo());
}
}












Amir said:
Hi,

I am trying to figure out how to zoom relative to the mouse pointer
position. Below, please find my sample code.

When I initially position the mouse pointer on one corner of the red
rectangle and use the mouse wheel, I am able to zoom in and out as
expected. As soon as I move the pointer position, let's say to another
corner of the red rectangle, and try to zoom in or out, my position
relative to the rectangle shifts around.

I think I may be missing a transform or two. In another attempt, in the
mouseWheelMoved method, I inverted tx from the previous iteration and
transformed the mouse position p with the inverted transform before
recalculating the new transform; but the behavior got more erratic.


Any help is greatly appreciated. What am I missing here?

Thanks.

Here is my code:

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;

@SuppressWarnings("serial")
public class ZoomDemo extends JPanel {

AffineTransform tx = new AffineTransform();

Rectangle2D.Double rect = new Rectangle2D.Double(100, 100, 20, 30);

public ZoomDemo() {
this.addMouseWheelListener(new ZoomHandler());
}

@Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2 = (Graphics2D) g;

Path2D.Double path;
g2.setColor(Color.RED);
path = new Path2D.Double(rect, tx);
g2.draw(path);
}

private class ZoomHandler implements MouseWheelListener {

Point oldPoint = null;

double scale = 1.0;

public void mouseWheelMoved(MouseWheelEvent e) {
if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {

scale += (.1 * e.getWheelRotation());
scale = Math.max(0.1, scale);
Point p = e.getPoint();

tx = AffineTransform.getTranslateInstance(p.getX(), p.getY());
tx.scale(scale, scale);
tx.translate(-p.getX(), -p.getY());

ZoomDemo.this.revalidate();
ZoomDemo.this.repaint();
}
}
}

public static void main(String[] args) {

JFrame f = new JFrame("ZoomDemo");
ZoomDemo zoomDemo = new ZoomDemo();
JScrollPane sp = new JScrollPane(zoomDemo);
f.getContentPane().add(sp);
f.setSize(500, 500);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
 
J

John B. Matthews

Amir Kouchekinia said:
I've finally figured this out. Here's my code if you are interested:
[...]
Point2D p1 = e.getPoint();
Point2D p2 = null;
try {
p2 = tx.inverseTransform(p1, null);
} catch (NoninvertibleTransformException ex) {
// should not get here
ex.printStackTrace();
return;
}
[...]

Very nice! Not _exactly_ the Desktop, but an interesting effect.

John
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,015
Latest member
AmbrosePal

Latest Threads

Top