I need some assistance figuring out how to translate coordinates from mouse events during zoom ... it works when zoom factor is 1.0 but not sure of algorithm when it changes...
I can drag the rectangle around the screen when if I comment out the zoom code but once zoom, the mouse coordinates screw up once the zoom is applied
I just cannot figure out the coordinates translation code
package ca.stackoverflow.main;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
public class Demo extends JFrame {
private static final long serialVersionUID = 1L;
public Demo() {
setPreferredSize(new Dimension(640, 480));
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
JScrollPane scroll = new JScrollPane(new Panel());
add(scroll, BorderLayout.CENTER);
pack();
setLocationRelativeTo(null);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
Demo demo = new Demo();
demo.setVisible(true);
}
catch(Throwable e) {
throw new RuntimeException(e);
}
}
});
}
}
class Panel extends JPanel implements MouseWheelListener, MouseListener, MouseMotionListener {
private static final long serialVersionUID = 1L;
private static final double MIN_ZOOM_FACTOR = 0.1;
private static final double MAX_ZOOM_FACTOR = 5.0;
private Color _fillColor;
private Point _startPoint;
private double xOffset;
private double yOffset;
private int xdragOffset;
private int ydragOffset;
private double zoomFactor = 1;
private double prevZoomFactor = 1;
private boolean zoomer;
private Rectangle _rectangle;
public Panel() {
setPreferredSize(new Dimension(1000, 1000));
addMouseWheelListener(this);
addMouseMotionListener(this);
addMouseListener(this);
_fillColor = Color.WHITE;
_rectangle = new Rectangle(50, 50, 100, 200);
}
@Override public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
AffineTransform at = new AffineTransform();
if (zoomer) {
double xRel = MouseInfo.getPointerInfo().getLocation().getX() - getLocationOnScreen().getX();
double yRel = MouseInfo.getPointerInfo().getLocation().getY() - getLocationOnScreen().getY();
double zoomDiv = zoomFactor / prevZoomFactor;
xOffset = (zoomDiv) * (xOffset) (1 - zoomDiv) * xRel;
yOffset = (zoomDiv) * (yOffset) (1 - zoomDiv) * yRel;
prevZoomFactor = zoomFactor;
zoomer = false;
}
at.translate(xOffset, yOffset);
at.scale(zoomFactor, zoomFactor);
g2.transform(at);
g.setColor(_fillColor);
g.fillRect(_rectangle.x, _rectangle.y, _rectangle.width, _rectangle.height);
g.setColor(Color.BLACK);
g.drawRect(_rectangle.x, _rectangle.y, _rectangle.width, _rectangle.height);
}
@Override public void mouseMoved(MouseEvent e) {
_fillColor = Color.WHITE;
if(_rectangle.contains(e.getPoint())) {
_fillColor = Color.GREEN;
}
repaint();
}
@Override public void mousePressed(MouseEvent e) {
_startPoint = null;
_fillColor = Color.WHITE;
if(_rectangle.contains(e.getPoint())) {
_startPoint = e.getPoint();
_fillColor = Color.GREEN;
xdragOffset = _startPoint.x - _rectangle.x;
ydragOffset = _startPoint.y - _rectangle.y;
}
repaint();
}
@Override public void mouseDragged(MouseEvent e) {
if(_startPoint != null) {
int diffX = e.getX() - _startPoint.x;
int diffY = e.getY() - _startPoint.y;
_rectangle.x = _startPoint.x diffX - xdragOffset;
_rectangle.y = _startPoint.y diffY - ydragOffset;
}
else {
_fillColor = Color.WHITE;
if(_rectangle.contains(e.getPoint())) {
_fillColor = Color.GREEN;
}
}
repaint();
}
@Override public void mouseWheelMoved(MouseWheelEvent e) {
zoomer = true;
if (e.getWheelRotation() < 0) {
zoomFactor = Math.max(zoomFactor / 1.1, MIN_ZOOM_FACTOR);
}
if (e.getWheelRotation() > 0) {
zoomFactor = Math.min(zoomFactor * 1.1, MAX_ZOOM_FACTOR);
}
repaint();
}
@Override public void mouseClicked(MouseEvent e) {}
@Override public void mouseReleased(MouseEvent e) {}
@Override public void mouseEntered(MouseEvent e) {}
@Override public void mouseExited(MouseEvent e) {}
}
CodePudding user response:
Okay, so, that was a little more involved than I first thought.
The "basic" concept is, you need to apply the same AffineTransformation
you used to paint the component to you _rectangle
So, I started by creating an instance property to keep track of the current transformation, as this is going to get re-used a bit
private AffineTransform transformation = new AffineTransform();
Then I added a getter
protected AffineTransform getTransformation() {
return transformation;
}
Then I "optimised" your paintComponent
, seriously...
double xRel = MouseInfo.getPointerInfo().getLocation().getX() - getLocationOnScreen().getX();
double yRel = MouseInfo.getPointerInfo().getLocation().getY() - getLocationOnScreen().getY();
is a really bad idea, generally, but especially from within a paint pass
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setTransform(getTransformation());
g2.setColor(_fillColor);
g2.fillRect(_rectangle.x, _rectangle.y, _rectangle.width, _rectangle.height);
g2.setColor(Color.BLACK);
g2.drawRect(_rectangle.x, _rectangle.y, _rectangle.width, _rectangle.height);
g2.dispose();
}
I then modified the mouseWheelMoved
to calculate the new AffineTransformation
each time it changed
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
Point2D zoomAnchor = e.getPoint();
prevZoomFactor = zoomFactor;
if (e.getWheelRotation() < 0) {
zoomFactor = Math.max(zoomFactor / 1.1, MIN_ZOOM_FACTOR);
}
if (e.getWheelRotation() > 0) {
zoomFactor = Math.min(zoomFactor * 1.1, MAX_ZOOM_FACTOR);
}
transformation = new AffineTransform();
double zoomDiv = zoomFactor / prevZoomFactor;
xOffset = (zoomDiv) * (xOffset) (1 - zoomDiv) * zoomAnchor.getX();
yOffset = (zoomDiv) * (yOffset) (1 - zoomDiv) * zoomAnchor.getX();
transformation.translate(xOffset, yOffset);
transformation.scale(zoomFactor, zoomFactor);
repaint();
}
Now, each time mouseMoved
is called, you need to apply the AffineTransformation
to the _rectangle
and check it for any collisions...
@Override
public void mouseMoved(MouseEvent e) {
AffineTransform at = getTransformation();
PathIterator pathIterator = _rectangle.getPathIterator(at);
GeneralPath shape = new GeneralPath();
shape.append(pathIterator, true);
_fillColor = Color.WHITE;
if (shape.contains(e.getPoint())) {
_fillColor = Color.GREEN;
}
repaint();
}
Now, I would be tempted to create a "shadow" Shape
each time mouseWheelMoved
is called, as you're going to want it for the other mouse events
Runnable example...
I've not updated your mousePressed
or mouseDragged
methods, I'll leave you to do that. I've also left in some debug code which will draw the "transformed shape" on each paint pass. This is draw BEFORE the Graphics
context is transformed, so it should give you a bit of a guide
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
public class Main extends JFrame {
private static final long serialVersionUID = 1L;
public Main() {
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
JScrollPane scroll = new JScrollPane(new TestPane());
add(scroll, BorderLayout.CENTER);
pack();
setLocationRelativeTo(null);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
Main demo = new Main();
demo.setVisible(true);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
});
}
class TestPane extends JPanel implements MouseWheelListener, MouseListener, MouseMotionListener {
private static final long serialVersionUID = 1L;
private static final double MIN_ZOOM_FACTOR = 0.1;
private static final double MAX_ZOOM_FACTOR = 5.0;
private Color _fillColor;
private Point _startPoint;
private double xOffset;
private double yOffset;
private int xdragOffset;
private int ydragOffset;
private double zoomFactor = 1;
private double prevZoomFactor = 1;
private boolean zoomer;
private Point zoomAnchor;
private Rectangle _rectangle;
private AffineTransform transformation = new AffineTransform();
public TestPane() {
addMouseWheelListener(this);
addMouseMotionListener(this);
addMouseListener(this);
_fillColor = Color.WHITE;
_rectangle = new Rectangle(50, 50, 100, 200);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(640, 480);
}
// This would be better pre-calculated when the
// zoom actually changes
protected AffineTransform getTransformation() {
return transformation;
}
private Shape testPath;
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
if (testPath != null) {
g2.setColor(Color.RED);
g2.draw(testPath);
}
g2.setTransform(getTransformation());
g2.setColor(_fillColor);
g2.fillRect(_rectangle.x, _rectangle.y, _rectangle.width, _rectangle.height);
g2.setColor(Color.BLACK);
g2.drawRect(_rectangle.x, _rectangle.y, _rectangle.width, _rectangle.height);
g2.dispose();
}
// This is purly for test purposes and you could simply
// create based on needs at the time
// Alternativly, you could apply a simular logic to it as the
// AffineTransformation and each time the zoom is changed,
// you could create a new, reusable, instance
protected Shape createTransformedShape() {
AffineTransform at = getTransformation();
PathIterator pathIterator = _rectangle.getPathIterator(at);
GeneralPath path = new GeneralPath();
path.append(pathIterator, true);
testPath = path;
return path;
}
@Override
public void mouseMoved(MouseEvent e) {
Shape shape = createTransformedShape();
_fillColor = Color.WHITE;
if (shape.contains(e.getPoint())) {
_fillColor = Color.GREEN;
}
repaint();
}
@Override
public void mousePressed(MouseEvent e) {
// _startPoint = null;
// _fillColor = Color.WHITE;
//
// AffineTransform at = getTransformation();
// Point2D zoomPoint = zoomedPointFrom(e.getPoint());
// PathIterator pathIterator = _rectangle.getPathIterator(at);
// GeneralPath path = new GeneralPath();
// path.append(pathIterator, true);
//
// if (path.contains(zoomPoint)) {
// _startPoint = e.getPoint();
// _fillColor = Color.GREEN;
// xdragOffset = _startPoint.x - _rectangle.x;
// ydragOffset = _startPoint.y - _rectangle.y;
// }
// repaint();
}
@Override
public void mouseDragged(MouseEvent e) {
// if (_startPoint != null) {
// int diffX = e.getX() - _startPoint.x;
// int diffY = e.getY() - _startPoint.y;
//
// _rectangle.x = _startPoint.x diffX - xdragOffset;
// _rectangle.y = _startPoint.y diffY - ydragOffset;
// } else {
// _fillColor = Color.WHITE;
// if (_rectangle.contains(e.getPoint())) {
// _fillColor = Color.GREEN;
// }
// }
// repaint();
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
Point2D zoomAnchor = e.getPoint();
prevZoomFactor = zoomFactor;
if (e.getWheelRotation() < 0) {
zoomFactor = Math.max(zoomFactor / 1.1, MIN_ZOOM_FACTOR);
}
if (e.getWheelRotation() > 0) {
zoomFactor = Math.min(zoomFactor * 1.1, MAX_ZOOM_FACTOR);
}
transformation = new AffineTransform();
double zoomDiv = zoomFactor / prevZoomFactor;
xOffset = (zoomDiv) * (xOffset) (1 - zoomDiv) * zoomAnchor.getX();
yOffset = (zoomDiv) * (yOffset) (1 - zoomDiv) * zoomAnchor.getX();
transformation.translate(xOffset, yOffset);
transformation.scale(zoomFactor, zoomFactor);
createTransformedShape();
repaint();
}
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
}
}