Hit testing, Moving objects
In this part of the Java 2D programming tutorial, we will first talk about hit testing. We will show, how to determine, if we have clicked inside a shape on a panel. In the second example, we will create two shapes, that we can move with a mouse on the panel and resize them with a mouse wheel. In the last example, we will be resizing a rectangle with two controlling points.
Hit testing
Hit testing is determining if we have clicked inside a Shape
with a mouse pointer. Each Shape has a contains() method.
The method tests if a specified Point2D is inside the
boundary of a Shape.
package com.zetcode;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class HitTesting extends JPanel {
private Rectangle2D rect;
private Ellipse2D ellipse;
private float alpha_rectangle;
private float alpha_ellipse;
public HitTesting() {
this.addMouseListener(new HitTestAdapter());
rect = new Rectangle2D.Float(20f, 20f, 80f, 50f);
ellipse = new Ellipse2D.Float(120f, 30f, 60f, 60f);
alpha_rectangle = 1f;
alpha_ellipse = 1f;
}
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D)g;
g2d.setColor(new Color(50, 50, 50));
RenderingHints rh =
new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
rh.put(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHints(rh);
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
alpha_rectangle));
g2d.fill(rect);
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
alpha_ellipse));
g2d.fill(ellipse);
}
public static void main(String[] args) {
JFrame frame = new JFrame("Hit testing");
frame.add(new HitTesting());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(250, 150);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
class RectRunnable implements Runnable {
private Thread runner;
public RectRunnable() {
runner = new Thread(this);
runner.start();
}
public void run() {
while (alpha_rectangle >= 0) {
repaint();
alpha_rectangle += -0.01f;
if (alpha_rectangle < 0) {
alpha_rectangle = 0;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
System.out.println("interrupted");
}
}
}
}
class HitTestAdapter extends MouseAdapter implements Runnable {
private RectRunnable rectAnimator;
private Thread ellipseAnimator;
public void mousePressed(MouseEvent e) {
int x = e.getX();
int y = e.getY();
if (rect.contains(x, y)) {
rectAnimator = new RectRunnable();
}
if (ellipse.contains(x, y)) {
ellipseAnimator = new Thread(this);
ellipseAnimator.start();
}
}
public void run() {
while (alpha_ellipse >= 0) {
repaint();
alpha_ellipse += -0.01f;
if (alpha_ellipse < 0) {
alpha_ellipse = 0;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
System.out.println("interrupted");
}
}
}
}
}
In our example, we have two Shapes.
A rectangle and a circle. By clicking on them they
gradually begin to fade away. In this example, we work
with Threads.
private Rectangle2D rect; private Ellipse2D ellipse;
We work with a rectangle and an ellipse.
private float alpha_rectangle; private float alpha_ellipse;
These two variables control the transparency of the two geometrical objects.
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
alpha_rectangle));
g2d.fill(rect);
Inside the paint() method, we set
the transparency of the rectangle.
The alpha_rectangle is computed
inside a dedicated Thread.
The HitTestAdapter class is responsible
for handling of mouse events. It does implement the
Runnable interface, which means that it
also creates the first thread.
if (ellipse.contains(x, y)) {
ellipseAnimator = new Thread(this);
ellipseAnimator.start();
}
If we press inside the ellipse a new Thread is created.
The thread calls the run() method. In our case,
it is the run() method of
the class itself. (HitTestAdapter)
if (rect.contains(x, y)) {
rectAnimator = new RectRunnable();
}
For the rectangle, we have a separate inner class.
A RectRunnable class. This class creates
its own thread in the constructor.
public void run() {
while (alpha_ellipse >= 0) {
repaint();
alpha_ellipse += -0.01f;
...
}
Note that the run() method is only called once.
To actually do something, we have to implement a while loop. The while
loop repaints the panel and decrements the alpha_ellipse
variable.
Moving and Scaling
In the next section we will learn how to move and scale graphical objects with a mouse on the panel. This is a very interesting piece of code. It can be used to move and scale charts, diagrams or other various objects in your application.
package com.zetcode;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class MovingScaling extends JPanel {
private ZRectangle zrect;
private ZEllipse zell;
public MovingScaling() {
MovingAdapter ma = new MovingAdapter();
addMouseMotionListener(ma);
addMouseListener(ma);
addMouseWheelListener(new ScaleHandler());
zrect = new ZRectangle(50, 50, 50, 50);
zell = new ZEllipse(150, 70, 80, 80);
setDoubleBuffered(true);
}
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D) g;
Font font = new Font("Serif", Font.BOLD, 40);
g2d.setFont(font);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setColor(new Color(0, 0, 200));
g2d.fill(zrect);
g2d.setColor(new Color(0, 200, 0));
g2d.fill(zell);
}
class ZEllipse extends Ellipse2D.Float {
public ZEllipse(float x, float y, float width, float height) {
setFrame(x, y, width, height);
}
public boolean isHit(float x, float y) {
if (getBounds2D().contains(x, y)) {
return true;
} else {
return false;
}
}
public void addX(float x) {
this.x += x;
}
public void addY(float y) {
this.y += y;
}
public void addWidth(float w) {
this.width += w;
}
public void addHeight(float h) {
this.height += h;
}
}
class ZRectangle extends Rectangle2D.Float {
public ZRectangle(float x, float y, float width, float height) {
setRect(x, y, width, height);
}
public boolean isHit(float x, float y) {
if (getBounds2D().contains(x, y)) {
return true;
} else {
return false;
}
}
public void addX(float x) {
this.x += x;
}
public void addY(float y) {
this.y += y;
}
public void addWidth(float w) {
this.width += w;
}
public void addHeight(float h) {
this.height += h;
}
}
class MovingAdapter extends MouseAdapter {
private int x;
private int y;
public void mousePressed(MouseEvent e) {
x = e.getX();
y = e.getY();
}
public void mouseDragged(MouseEvent e) {
int dx = e.getX() - x;
int dy = e.getY() - y;
if (zrect.isHit(x, y)) {
zrect.addX(dx);
zrect.addY(dy);
repaint();
}
if (zell.isHit(x, y)) {
zell.addX(dx);
zell.addY(dy);
repaint();
}
x += dx;
y += dy;
}
}
class ScaleHandler implements MouseWheelListener {
public void mouseWheelMoved(MouseWheelEvent e) {
int x = e.getX();
int y = e.getY();
if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {
if (zrect.isHit(x, y)) {
float amount = e.getWheelRotation() * 5f;
zrect.addWidth(amount);
zrect.addHeight(amount);
repaint();
}
if (zell.isHit(x, y)) {
float amount = e.getWheelRotation() * 5f;
zell.addWidth(amount);
zell.addHeight(amount);
repaint();
}
}
}
}
public static void main(String[] args) {
JFrame frame = new JFrame("Moving and Scaling");
frame.add(new MovingScaling());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(300, 300);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
In our code example, we have two graphical objects. A rectangle and a circle. You can move both by clicking on them and dragging them. You can also scale them up or down by positioning the mouse cursor over the objects and moving the mouse wheel.
private ZRectangle zrect; private ZEllipse zell;
As we have already mentioned, we have a rectangle and an ellipse on our panel. Both classes extend the functionality of a built-in classes from Java AWT package.
addMouseMotionListener(ma); addMouseListener(ma); addMouseWheelListener(new ScaleHandler());
We register three listeners. These listeners will capture mouse press, mouse drag and mouse wheel events.
class ZEllipse extends Ellipse2D.Float {
public ZEllipse(float x, float y, float width, float height) {
setFrame(x, y, width, height);
}
public boolean isHit(float x, float y) {
if (getBounds2D().contains(x, y)) {
return true;
} else {
return false;
}
}
...
}
This code excerpt shows a ZEllipse class.
It extends the built-in Ellipse2D.Float class.
It adds functionality for scaling and moving an ellipse.
For example, the isHit() method determines,
if the mouse pointer is inside the area of an ellipse.
The MovingAdapter class handles the mouse
press and mouse drag events.
public void mousePressed(MouseEvent e) {
x = e.getX();
y = e.getY();
}
In the mousePressed() method, we remember the
initial x, y coordinates of the object.
int dx = e.getX() - x; int dy = e.getY() - y;
Inside the mouseDragged() method, we calculate
the distance by which we have dragged the object.
if (zrect.isHit(x, y)) {
zrect.addX(dx);
zrect.addY(dy);
repaint();
}
Here if we are inside the area of the rectangle, we update the x, y coordinates of the rectangle and repaint the panel.
x += dx; y += dy;
The initial coordinates are updated.
The ScaleHandler class handles the scaling of the objects.
if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {
if (zrect.isHit(x, y)) {
float amount = e.getWheelRotation() * 5f;
zrect.addWidth(amount);
zrect.addHeight(amount);
repaint();
}
...
}
Here if we move a mouse wheel and our cursor is inside the area of
a rectangle, the rectangle is resized and the
panel repainted. The amount of the scaling is computed from the
getWheelRotation() method, which returns
the amount of the wheel rotation.
Resize Rectangle
In the next example, we will show how to resize a shape. Our shape will be a rectangle. On our rectangle, we will draw two small black rectangles. By clicking on these tiny rectangles and dragging them, we can resize our "big" rectangle.
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class ResizeRectangle extends JPanel {
private Point2D[] points;
private int SIZE = 8;
private int pos;
public ResizeRectangle() {
addMouseListener(new ShapeTestAdapter());
addMouseMotionListener(new ShapeTestAdapter());
pos = -1;
points = new Point2D[2];
points[0] = new Point2D.Double(50, 50);
points[1] = new Point2D.Double(150, 100);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
for (int i = 0; i < points.length; i++) {
double x = points[i].getX() - SIZE / 2;
double y = points[i].getY() - SIZE / 2;
g2.fill(new Rectangle2D.Double(x, y, SIZE, SIZE));
}
Rectangle2D s = new Rectangle2D.Double();
s.setFrameFromDiagonal(points[0], points[1]);
g2.draw(s);
}
private class ShapeTestAdapter extends MouseAdapter {
public void mousePressed(MouseEvent event) {
Point p = event.getPoint();
for (int i = 0; i < points.length; i++) {
double x = points[i].getX() - SIZE / 2;
double y = points[i].getY() - SIZE / 2;
Rectangle2D r = new Rectangle2D.Double(x, y, SIZE, SIZE);
if (r.contains(p)) {
pos = i;
return;
}
}
}
public void mouseReleased(MouseEvent event) {
pos = -1;
}
public void mouseDragged(MouseEvent event) {
if (pos == -1)
return;
points[pos] = event.getPoint();
repaint();
}
}
public static void main(String[] args) {
JFrame frame = new JFrame("Resize Rectangle");
frame.add(new ResizeRectangle());
frame.setSize(300, 300);
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
Basically, there are two ways to create a rectangle. By providing x, y coordinates plus the width and height of the rectangle. Another way is to provide the top-left and bottom-right points. In our code example, we will use both methods.
private Point2D[] points;
In this array, we will store points, that will make our rectangle.
private int SIZE = 8;
This is the size of the small black rectangles.
points = new Point2D[2]; points[0] = new Point2D.Double(50, 50); points[1] = new Point2D.Double(150, 100);
These are the initial coordinates for a rectangle.
Rectangle2D s = new Rectangle2D.Double(); s.setFrameFromDiagonal(points[0], points[1]); g2.draw(s);
Here we draw a rectangle from the points.
for (int i = 0; i < points.length; i++) {
double x = points[i].getX() - SIZE / 2;
double y = points[i].getY() - SIZE / 2;
g2.fill(new Rectangle2D.Double(x, y, SIZE, SIZE));
}
This code draws the two small controlling rectangles.
public void mousePressed(MouseEvent event) {
Point p = event.getPoint();
for (int i = 0; i < points.length; i++) {
double x = points[i].getX() - SIZE / 2;
double y = points[i].getY() - SIZE / 2;
Rectangle2D r = new Rectangle2D.Double(x, y, SIZE, SIZE);
if (r.contains(p)) {
pos = i;
return;
}
}
}
In the mousePressed() method, we determine,
if we have clicked inside one of the
two controlling points. If we hit one of them, the
pos variable stores which of them it was.
public void mouseDragged(MouseEvent event) {
if (pos == -1)
return;
points[pos] = event.getPoint();
repaint();
}
Here the rectangle is dynamically resized. During the
mouseDragged()
event, we get the current point, update our array of
points and repaint the panel.
In this part of the Java 2D tutorial, we covered hit testing and moving objects.