I have a jpanel that I draw on. I would like to be able to zoom in and out using the mousewheel, but I want to zoom to the location of the mouse, such that the point under the mouse stays the same. I have found some questions here on stackoverflow, but they did not work for me.
I got pretty close to doing what I want by implementing what is described here. Here is my code:
public class MyPanel extends JPanel {
...
private double zoom = 1;
private double zoom_old = 1;
private int zoomPointX;
private int zoomPointY;
...
class CustomMouseWheelListener implements MouseWheelListener {
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
zoomPointX = e.getX();
zoomPointY = e.getY();
if (e.getPreciseWheelRotation() < 0) {
zoom -= 0.1;
} else {
zoom += 0.1;
}
if (zoom < 0.01) {
zoom = 0.01;
}
repaint();
}
}
...
protected void paintComponent(Graphics g) {
Graphics2D g2D = (Graphics2D) g;
super.paintComponent(g2D);
if (zoom != zoom_old) {
double scalechange = zoom - zoom_old;
zoom_old = zoom;
double offsetX = -(zoomPointX * scalechange);
double offsetY = -(zoomPointY * scalechange) ;
AffineTransform at = new AffineTransform();
at.scale(zoom, zoom);
at.translate(offsetX, offsetY);
g2D.setTransform(at);
}
a_different_class_where_i_do_some_drawing.draw(g2D);
}
}
This ALMOST does what I want. If I try to zoom, I notice that the position of the mouse is taken into account, so for example If I have my mouse on the left of the panel it will roughly zoom in on the left. However, it does not zoom exactly onto the mouse, so the point under the mouse still changes.
Can anyone help me fix this?
EDIT:
Here is a picture of what is happening with the code posted above: I start out with the mouse on the blue square and then just scroll with the mouse wheel. As you can see, the mouse position if being changed:
I solved the problem by implementing what is being described here
Here is the updated code:
public class MyPanel extends JPanel {
...
private double zoom = 1;
private int zoomPointX;
private int zoomPointY;
...
class CustomMouseWheelListener implements MouseWheelListener {
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
zoomPointX = e.getX();
zoomPointY = e.getY();
if (e.getPreciseWheelRotation() < 0) {
zoom -= 0.1;
} else {
zoom += 0.1;
}
if (zoom < 0.01) {
zoom = 0.01;
}
repaint();
}
}
...
protected void paintComponent(Graphics g) {
Graphics2D g2D = (Graphics2D) g;
super.paintComponent(g2D);
AffineTransform at = g2D.getTransform();
at.translate(zoomPointX, zoomPointY);
at.scale(zoom, zoom);
at.translate(-zoomPointX, -zoomPointY);
g2D.setTransform(at);
a_different_class_where_i_do_some_drawing.draw(g2D);
}
}
Related
I have a Brush class
final class Brush
{
private final int size;
private final Color color;
private final Ellipse2D.Double blob=new Ellipse2D.Double();
private Brush(int size,Color color)
{
this.size=size;
this.color=color;
}
void paint(Graphics2D g2d,Point location)
{
g2d.setColor(color);
blob.setFrame(location.x-(size/2.0),location.y-(size/2.0),size,size);//Translate ecllipse so that the centre of it's bounding box is exactly at the cursor location for more accurate blobs
g2d.fill(blob);
}
}
I have a Blob class which keeps track of the user's current brush settings and where the user previously dragged his mouse so as to remember to draw a blob there.
final class Blob
{
final Brush brush;
final Point location;
private Blob(Brush brush,Point location)
{
this.brush=brush;
this.location=location;
}
private void paint(Graphics2D g){brush.paint(g,location);}
}
Finally my paint logic which is very simple.
Whenever the user drags his mouse add a blob at that current location with the current brush settings and inside paint() loop through all blobs and redraw them.
final class Painter extends Canvas
{
private Brush brush=new Brush(5,Color.red);//Can Change
private final ArrayList<Blob> blobs=new ArrayList();
private Painter(){addMouseMotionListener(new Dragger());}
#Override
public void paint(Graphics g)
{
super.paint(g);
blobs.forEach(blob->blob.paint(g));
}
private final class Dragger extends MouseAdapter
{
#Override
public void mouseDragged(MouseEvent m)
{
blobs.add(brush,m.getPoint());
repaint();
}
}
}
You can already see the problem here. The size of the list grows exponentially and my app quickly slows down. Is there an more efficient way to do this?
The much more efficient way to do this is to use a BufferedImage for your drawing, and than painting the BufferedImage in paintComponent
Code taken from PaintArea:
public void paintComponent(Graphics g) {
if (mSizeChanged) {
handleResize();
}
g.drawImage(mImg, 0, 0, null);
}
protected class MListener extends MouseAdapter implements MouseMotionListener {
Point mLastPoint;
public void mouseDragged(MouseEvent me) {
Graphics g = mImg.getGraphics();
if ((me.getModifiers() & InputEvent.BUTTON1_MASK) != 0) {
g.setColor(mColor1);
} else {
g.setColor(mColor2);
}
Point p = me.getPoint();
if (mLastPoint == null) {
g.fillOval(p.x - mBrushSize / 2, p.y - mBrushSize / 2, mBrushSize, mBrushSize);
//g.drawLine(p.x, p.y, p.x, p.y);
}
else {
g.drawLine(mLastPoint.x, mLastPoint.y, p.x, p.y);
//g.fillOval(p.x - mBrushSize / 2, p.y - mBrushSize / 2, mBrushSize, mBrushSize);
double angle = MathUtils.angle(mLastPoint, p);
if (angle < 0) {
angle += 2 * Math.PI;
}
#SuppressWarnings("unused")
double distance = MathUtils.distance(mLastPoint, p) * 1.5;
if (angle < Math.PI / 4 || angle > 7 * Math.PI / 4 || Math.abs(Math.PI - angle) < Math.PI / 4) {
for (int i = 0; i < mBrushSize / 2; i ++) {
g.drawLine(mLastPoint.x, mLastPoint.y + i, p.x, p.y + i);
g.drawLine(mLastPoint.x, mLastPoint.y - i, p.x, p.y - i);
}
}
else {
for (int i = 0; i < mBrushSize / 2; i ++) {
g.drawLine(mLastPoint.x + i, mLastPoint.y, p.x + i, p.y);
g.drawLine(mLastPoint.x - i, mLastPoint.y, p.x - i, p.y);
}
}
}
mLastPoint = p;
g.dispose();
repaint();
}
public void mouseMoved(MouseEvent me) {}
public void mouseReleased(MouseEvent me) {
mLastPoint = null;
}
}
when i zoom JPanel it zooms and its components zooms but it converts to painted component not the component its self which i can't control it or type on it if it JTextField and the original component stay at its size and location as shown in picture
panel after zooming
and this is my code
i make the panel which i want to zoom here in this class
public class designPanel extends JPanel{
public double zoomFactor = 1;
public boolean zoomer = true;
public AffineTransform at;
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
if (zoomer == true) {
at = g2.getTransform();
at.scale(zoomFactor, zoomFactor);
zoomer = false;
}
g2.transform(at);
}
public void setZoomFactor(double factor){
if(factor<this.zoomFactor){
this.zoomFactor=this.zoomFactor/1.1;
}
else{
this.zoomFactor=factor;
}
this.zoomer=true;
}
public double getZoomFactor() {
return zoomFactor;
}
}
and then zoom in and out this panel by mouse wheel in this code
design_gridPanel.addMouseWheelListener(new MouseWheelListener() {
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
if (e.getWheelRotation() < 0) {
design_gridPanel.setZoomFactor(1.1 * design_gridPanel.getZoomFactor());
design_gridPanel.revalidate();
design_gridPanel.repaint();
}
if (e.getWheelRotation() > 0) {
design_gridPanel.setZoomFactor(design_gridPanel.getZoomFactor() / 1.1);
design_gridPanel.revalidate();
design_gridPanel.repaint();
}
}});
I am trying to create a simple drawing application using swing and java2D.
The aim is to achieve a smooth zoom, always relative to the mouse cursor point.
The application consists of two classes: CanvasPane and Canvas.
CanvasPane class is a simple container with BorderLayout and JScrollPane in the center.
Canvas class is a drawing component added to a JScrollPane in CanvasPane.
Canvas draws a simple rectangle[800x600], and dispatches it's mouse events (wheel and drag).
When rectangle is smaller then visibleRect, canvas size is equal to visibleRect and I call AffineTransform.translate to follow mouse
(thanks to this question)
When rectangle grows bigger then canvas, canvas size grows too and became scrollable. Then I call scrollRectToVisible on it to follow mouse.
The question is:
How to use translate and scrollRectToVisible together, to smooth scale without graphics jumps. May be there is some known decision?
What I want is perfectly realized in YED Graph Editor, but it's code is closed.
I have tried with many examples but there were only zoom or scroll without complex usage of them.
Full code follows.
Class CanvasPane:
import javax.swing.*;
import java.awt.*;
public class CanvasPane extends JPanel {
private Canvas canvas;
public CanvasPane(boolean isDoubleBuffered) {
super(isDoubleBuffered);
setLayout(new BorderLayout());
canvas = new Canvas(1.0);
JScrollPane pane = new JScrollPane(canvas);
pane.getViewport().setBackground(Color.DARK_GRAY);
add(pane, BorderLayout.CENTER);
}
public static void main(String[] args) {
JFrame frame = new JFrame("Test Graphics");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new CanvasPane(true), BorderLayout.CENTER);
frame.setSize(new Dimension(1000, 800));
frame.setVisible(true);
}
}
Class Canvas:
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
public class Canvas extends JComponent implements MouseWheelListener, MouseMotionListener, MouseListener {
private double zoom = 1.0;
public static final double SCALE_STEP = 0.1d;
private Dimension initialSize;
private Point origin;
private double previousZoom = zoom;
AffineTransform tx = new AffineTransform();
private double scrollX = 0d;
private double scrollY = 0d;
private Rectangle2D rect = new Rectangle2D.Double(0,0, 800, 600);
public Canvas(double zoom) {
this.zoom = zoom;
addMouseWheelListener(this);
addMouseMotionListener(this);
addMouseListener(this);
setAutoscrolls(true);
}
public Dimension getInitialSize() {
return initialSize;
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
Graphics2D g2d = (Graphics2D) g.create();
g2d.clearRect(0, 0, getWidth(), getHeight());
g2d.transform(tx);
g2d.setColor(Color.DARK_GRAY);
g2d.fill(rect);
g2d.setColor(Color.GRAY);
g2d.setStroke(new BasicStroke(5.0f));
g2d.draw(rect);
g2d.dispose();
}
#Override
public void setSize(Dimension size) {
super.setSize(size);
if (initialSize == null) {
this.initialSize = size;
}
}
#Override
public void setPreferredSize(Dimension preferredSize) {
super.setPreferredSize(preferredSize);
if (initialSize == null) {
this.initialSize = preferredSize;
}
}
public void mouseWheelMoved(MouseWheelEvent e) {
double zoomFactor = - SCALE_STEP*e.getPreciseWheelRotation()*zoom;
zoom = Math.abs(zoom + zoomFactor);
//Here we calculate new size of canvas relative to zoom.
Rectangle realView = getVisibleRect();
Dimension d = new Dimension(
(int)(initialSize.width*zoom),
(int)(initialSize.height*zoom));
// if (d.getWidth() >= realView.getWidth() && d.getHeight() >= realView.getHeight()) {
setPreferredSize(d);
setSize(d);
validate();
followMouseOrCenter(e);
// }
//Here we calculate transform for the canvas graphics to scale relative to mouse
translate(e);
repaint();
previousZoom = zoom;
}
private void translate(MouseWheelEvent e) {
Rectangle realView = getVisibleRect();
Point2D p1 = e.getPoint();
Point2D p2 = null;
try {
p2 = tx.inverseTransform(p1, null);
} catch (NoninvertibleTransformException ex) {
ex.printStackTrace();
return;
}
Dimension d = getSize();
if (d.getWidth() <= realView.getWidth() && d.getHeight() <= realView.getHeight()) {
//Zooming and translating relative to the mouse position
tx.setToIdentity();
tx.translate(p1.getX(), p1.getY());
tx.scale(zoom, zoom);
tx.translate(-p2.getX(), -p2.getY());
} else {
//Only zooming, translate is not needed because scrollRectToVisible works;
tx.setToIdentity();
tx.scale(zoom, zoom);
}
// What to do next?
// The only translation works when rect is smaller then canvas size.
// Rect bigger then canvas must be scrollable, but relative to mouse position as before.
// But when the rect gets bigger than canvas, there is a terrible jump of a graphics.
//So there must be some combination of translation ans scroll to achieve a smooth scale.
//... brain explosion(((
}
public void followMouseOrCenter(MouseWheelEvent e) {
Point2D point = e.getPoint();
Rectangle visibleRect = getVisibleRect();
scrollX = point.getX()/previousZoom*zoom - (point.getX()-visibleRect.getX());
scrollY = point.getY()/previousZoom*zoom - (point.getY()-visibleRect.getY());
visibleRect.setRect(scrollX, scrollY, visibleRect.getWidth(), visibleRect.getHeight());
scrollRectToVisible(visibleRect);
}
public void mouseDragged(MouseEvent e) {
if (origin != null) {
int deltaX = origin.x - e.getX();
int deltaY = origin.y - e.getY();
Rectangle view = getVisibleRect();
Dimension size = getSize();
view.x += deltaX;
view.y += deltaY;
scrollRectToVisible(view);
}
}
public void mouseMoved(MouseEvent e) {
}
public void mouseClicked(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
origin = new Point(e.getPoint());
}
public void mouseReleased(MouseEvent e) {
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
}
I have finally achieved the enlightenment=)
We can simply make the canvas size much bigger than size of a drawing object, and so forget about calculating of any unintelligible transforms.
I initially make the canvas 100x bigger than drawing rectangle.
Then I zoom Graphics2D and translate zoomed graphics to the center of the canvas while painting. Next, I calculate a new visibleRect to follow mouse point and scroll to it.
When canvas became unscrollable, it's unreasonable to follow mouse because the drawing object is too small (100x smaller then its initial size), so I only center it to be always visible.
It works exactly as I needed.
So we have a working example with zoom following mouse and drag by mouse.
Code follows.
Class CanvasPane:
import javax.swing.*;
import java.awt.*;
public class CanvasPane extends JPanel {
private static Canvas canvas;
public CanvasPane(boolean isDoubleBuffered) {
super(isDoubleBuffered);
setLayout(new BorderLayout());
canvas = new Canvas(1.0);
JScrollPane pane = new JScrollPane(canvas);
pane.getViewport().setBackground(Color.DARK_GRAY);
add(pane, BorderLayout.CENTER);
}
public static void main(String[] args) {
JFrame frame = new JFrame("Test Graphics");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new CanvasPane(true), BorderLayout.CENTER);
frame.setSize(new Dimension(1000, 800));
frame.pack();
frame.setVisible(true);
//Initial scrolling of the canvas to its center
Rectangle rect = canvas.getBounds();
Rectangle visibleRect = canvas.getVisibleRect();
double tx = (rect.getWidth() - visibleRect.getWidth())/2;
double ty = (rect.getHeight() - visibleRect.getHeight())/2;
visibleRect.setBounds((int)tx, (int)ty, visibleRect.width, visibleRect.height);
canvas.scrollRectToVisible(visibleRect);
}
}
Class Canvas:
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
public class Canvas extends JComponent implements MouseWheelListener, MouseMotionListener, MouseListener {
private double zoom = 1.0;
public static final double SCALE_STEP = 0.1d;
private Dimension initialSize;
private Point origin;
private double previousZoom = zoom;
private double scrollX = 0d;
private double scrollY = 0d;
private Rectangle2D rect = new Rectangle2D.Double(0,0, 800, 600);
private float hexSize = 3f;
public Canvas(double zoom) {
this.zoom = zoom;
addMouseWheelListener(this);
addMouseMotionListener(this);
addMouseListener(this);
setAutoscrolls(true);
//Set preferred size to be 100x bigger then drawing object
//So the canvas will be scrollable until our drawing object gets 100x smaller then its natural size.
//When the drawing object became so small, it is unnecessary to follow mouse on it,
//and we only center it on the canvas
setPreferredSize(new Dimension((int)(rect.getWidth()*100), (int)(rect.getHeight()*100)));
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
//Obtain a copy of graphics object without any transforms
Graphics2D g2d = (Graphics2D) g.create();
g2d.clearRect(0, 0, getWidth(), getHeight());
//Zoom graphics
g2d.scale(zoom, zoom);
//translate graphics to be always in center of the canvas
Rectangle size = getBounds();
double tx = ((size.getWidth() - rect.getWidth() * zoom) / 2) / zoom;
double ty = ((size.getHeight() - rect.getHeight() * zoom) / 2) / zoom;
g2d.translate(tx, ty);
//Draw
g2d.setColor(Color.LIGHT_GRAY);
g2d.fill(rect);
g2d.setColor(Color.DARK_GRAY);
g2d.setStroke(new BasicStroke(5.0f));
g2d.draw(rect);
//Forget all transforms
g2d.dispose();
}
#Override
public void setSize(Dimension size) {
super.setSize(size);
if (initialSize == null) {
this.initialSize = size;
}
}
#Override
public void setPreferredSize(Dimension preferredSize) {
super.setPreferredSize(preferredSize);
if (initialSize == null) {
this.initialSize = preferredSize;
}
}
public void mouseWheelMoved(MouseWheelEvent e) {
double zoomFactor = - SCALE_STEP*e.getPreciseWheelRotation()*zoom;
zoom = Math.abs(zoom + zoomFactor);
//Here we calculate new size of canvas relative to zoom.
Dimension d = new Dimension(
(int)(initialSize.width*zoom),
(int)(initialSize.height*zoom));
setPreferredSize(d);
setSize(d);
validate();
followMouseOrCenter(e.getPoint());
previousZoom = zoom;
}
public void followMouseOrCenter(Point2D point) {
Rectangle size = getBounds();
Rectangle visibleRect = getVisibleRect();
scrollX = size.getCenterX();
scrollY = size.getCenterY();
if (point != null) {
scrollX = point.getX()/previousZoom*zoom - (point.getX()-visibleRect.getX());
scrollY = point.getY()/previousZoom*zoom - (point.getY()-visibleRect.getY());
}
visibleRect.setRect(scrollX, scrollY, visibleRect.getWidth(), visibleRect.getHeight());
scrollRectToVisible(visibleRect);
}
public void mouseDragged(MouseEvent e) {
if (origin != null) {
int deltaX = origin.x - e.getX();
int deltaY = origin.y - e.getY();
Rectangle view = getVisibleRect();
view.x += deltaX;
view.y += deltaY;
scrollRectToVisible(view);
}
}
public void mouseMoved(MouseEvent e) {
}
public void mouseClicked(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
origin = new Point(e.getPoint());
}
public void mouseReleased(MouseEvent e) {
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
}
I have the following code:
import java.awt.*;
import javax.swing.*;
import java.util.ArrayList;
import java.util.List;
public class Ball extends JPanel implements Runnable {
List<Ball> balls = new ArrayList<Ball>();
Color color;
int diameter;
long delay;
private int x;
private int y;
private int vx;
private int vy;
public Ball(String ballcolor, int xvelocity, int yvelocity) {
color = Color.PINK;
diameter = 30;
delay = 40;
x = 1;
y = 1;
vx = xvelocity;
vy = yvelocity;
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(color);
g.fillOval(x,y,50,50); //adds color to circle
g.setColor(Color.pink);
g2.drawOval(x,y,50,50); //draws circle
}
public void run() {
while(isVisible()) {
try {
Thread.sleep(delay);
} catch(InterruptedException e) {
System.out.println("interrupted");
}
move();
repaint();
}
}
public void move() {
if(x + vx < 0 || x + diameter + vx > getWidth()) {
vx *= -1;
}
if(y + vy < 0 || y + diameter + vy > getHeight()) {
vy *= -1;
}
x += vx;
y += vy;
}
private void start() {
while(!isVisible()) {
try {
Thread.sleep(25);
} catch(InterruptedException e) {
System.exit(1);
}
}
Thread thread = new Thread(this);
thread.setPriority(Thread.NORM_PRIORITY);
thread.start();
}
public static void main(String[] args) {
Ball ball1 = new Ball("pink",6,6);
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().setBackground(Color.BLACK);
f.getContentPane().add(ball1);
f.setSize(1000,500);
f.setLocation(200,200);
new Thread(ball1).start();
f.setVisible(true);
}
}
I would like to make the background black instead of the default grey one, but this only works if I get the ball out of the code, basically deleting f.getContentPane().add(ball1);. What I want is a pink ball bouncing on a black background, but it keeps going back to grey when adding the ball.
What is the problem?
Ball which extends from JPanel is opaque (not see through), so it is filling it's own background with it's default color...
You could make Ball transparent by using setOpaque(false) or set the Ball's background color to BLACK using setBackground(Color.BLACK) for example...
ps- I should add, because JFrame uses a BorderLayout, your Ball pane will occupy the entire available space within the center of the frame (CENTER being the default position and the frame not containing any other components)
I am having some trouble figuring out how to determine if a mouseclick event was clicked inside of a rectangle, if the rectangle has been rotated.
Lets say I have a MouseAdapter as simple as this. It just prints out a statement saying that we hit inside the rectangle if the mousePressed was in fact within the rectangle.
MouseAdapter mAdapter = new MouseAdapter() {
public void mousePressed(MouseEvent e) {
int xPos = e.getX();
int yPos = e.getY();
if(xPos >= rect.x && xPos <= rect.x + rect.width && yPos >= rect.y && yPos <= rect.y + rect.height) {
System.out.println("HIT INSIDE RECTANGLE");
}
}
};
My issue comes from when I rotate the rectangle. The if statement above obviously doesn't consider the rotation, so after I rotate the rectangle, my hit test fails. For rotate, I'm doing something as simple as this in a paint() function:
class drawRect {
Rectangle rect = new Rectangle();
...
public void paint(Graphics g) {
Graphcis2D g2 = (Graphics2D) g;
AffineTransform old = g2.getTransform();
g.rotate(Math.toRadians(90), rect.x, rect.y);
g2.draw(rect);
g2.setTransform(old);
}
}
This is just some quick pseudocode, so that you guys can understand what I am trying to do. So please don't worry about syntax and all of that. Any help would be appreciated!
You could apply the rotation to your mouse coordinates as well. Dry-coded:
MouseAdapter mAdapter = new MouseAdapter() {
public void mousePressed(MouseEvent e) {
// Create the same transform as used for drawing the rectangle
AffineTransform t = new AffineTransform();
t.rotate(Math.toRadians(90), rect.x, rect.y);
Point2D tp = t.inverseTransform(e.getPoint());
if(rect.contains(tp)) {
System.out.println("HIT INSIDE RECTANGLE");
}
}
};