Reputation: 9040
I am writing a program where the user can draw points on a JPanel by clicking and dragging the mouse. In addition, the drawing area is divided into a number of sectors, and the points are rotated such that every sector is the same. For example, a twelve sector arrangement with a single point inside will rotate that point twelve times through 360/12 degrees. The rotation works fine, but there is some very strange behaviour when trying to draw points. If one attempts to draw a circle around the origin, the points will appear very sporadically for a short time, before being added smoothly. This image shows what I mean (the result of drawing a quarter circle in one of the sectors):
You can see that, when approaching one side of a sector division, the points are added smoothly. However, initially the points are separated and not smoothly being drawn. The code is shown below (the superfluous GUI elements and imports have been removed for ease of reading):
public class Doiles extends JPanel implements MouseListener,ActionListener,MouseMotionListener
{
//global variable declarations
JFrame window = new JFrame("Draw");
final int linelength = 340;//length of sector defining lines
int nlines = 12;//store the number of sector defining lines
String numsectors=null;
int currentovalsize = 10;
Color currentcolour = Color.WHITE;
Deque<DoilyPoint> points = new LinkedList<DoilyPoint>();
public Doiles()
{
window.setSize(2000,1000);
//drawing panel + paint method
JPanel drawingPanel = new JPanel()
{
public void paintComponent(Graphics g)
{
super.paintComponent(g);
//calculate angle between sectors
double theta = (2*Math.PI)/nlines;
g.setColor(Color.WHITE);
//calculate line coordinates and draw the sector lines
for(int i=0; i <nlines;i++)
{
g.drawLine(400, 350, 400+(int)Math.round(linelength*Math.cos(theta*i)), 350+(int)Math.round(linelength*Math.sin(theta*i)));
}
for(DoilyPoint j : points)
{
g.fillOval(j.getX(), j.getY(), j.getSize(), j.getSize());
for(int h = 1;h<nlines;h++)
{
double rtheta;
if(j.getX()==400)
rtheta = Math.PI/2;
else
rtheta = Math.atan((j.getY()-350)/(j.getX()-400));
System.out.println(rtheta);
double r = Math.sqrt(Math.pow(j.getX()-400,2)+Math.pow(j.getY()-350,2));
double angle = (h*theta)+rtheta;
double x = r*Math.cos(angle);
double y = r*Math.sin(angle);
g.fillOval((int)Math.round(x)+400,(int)Math.round(y)+350, j.getSize(), j.getSize());
}
}
}
};
}
public static void main(String[] args)
{
new Doiles();
}
public void addPoint(int x, int y)
{
points.addFirst(new DoilyPoint(currentovalsize,x,y,currentcolour));
window.repaint();
}
@Override
public void mouseDragged(MouseEvent e)
{
addPoint(e.getX(),e.getY());
}
}
class DoilyPoint
{
private int size;
private int x;
private int y;
private Color colour;
void setSize(int a){this.size = a;}
int getSize(){return size;}
void setX(int a){this.x =a;}
int getX(){return x;}
void setY(int a){this.y = a;}
int getY(){return y;}
void setColor(Color r){this.colour = r;}
Color getColor(){return colour;}
public DoilyPoint(int size,int x, int y,Color colour)
{
this.size = size;
this.x = x;
this.y = y;
this.colour = colour;
}
}
I assume it's something to do with the way Java handles dragging the mouse, but I'd like to know how to smooth out the drawing. Can anyone tell me what's wrong?
Upvotes: 2
Views: 227
Reputation: 1225
Yes, the mouse is refreshing at a very fast rate causing it to lag. Calling repaint() on every mouse moved on a 1080p screen would refresh it far too many times than are necessary. There are two ways to solve this. Limiting the calls to addPoint() by:
I'll provide an example of how to do it both ways.
Save the location of the last point that was updated in an instance variable in Doiles class:
int previousX, previousY
Set an offset value (distance moved) that must be met before drawing and repainting the screen:
static final in MINIMUM_OFFSET = 10; //mess around with this and use whatever looks good and performs well
Then, modify your mouseDragged implementation to account for the distance it moved:
@Override
public void mouseDragged(MouseEvent e)
{
//you can add some trig to this to calculate the hypotenuse, but with pixels I wouldn't bother
int distance = Math.abs(e.getY() - previousY) + Math.abs(e.getX - previousX);
if(distance > OFFSET_VALUE){
//update the previous x,y values
this.previousX = e.getX();
this.previousY = e.getY();
//add point
addPoint(e.getX(),e.getY());
}
}
This will reduce the refresh rate, dependent upon the distance the mouse moves. This works for what you described, but if there are other factors to be accounted for with this JPanel, the Time solution below would work better:
You don't really need to implement MouseMotionListener for this one. In your MouseListener implementation, change a boolean flag in your class that represents whether the mouse is pressed on the JPanel:
boolean isMousePressed;
@Override
mousePressed(MouseEvent e) {
isMousePressed = true;
}
@Override
mouseReleased(MouseEvent e) {
isMousePressed = false;
}
Then, use a javax.swing.Timer
(Thread-safe for Swing components) to update it the canvas every so often using MouseListener + PointerInfo:
// this is set to 60Hz I, mess around with it to get the best results
Timer timer=new Timer(1000/60, e -> {
if(isMousePressed) {
Point p = MouseInfo.getPointerInfo().getLocation();
addPoint(p.x,p.y);
}
});
Personally, I prefer the second solution as it's less intensive, and the refresh rate is constant.
EDIT: combine the first one with the second one by reassigning a 'Point' instance variable every time the Timer gets called and the mouse is pressed down. Then you'll have even refresh rate AND consistent dot placement.
Upvotes: 2
Reputation: 347334
Why it's not working will take someone with much better mathematical skills then I have to figure out, I'll ask my 4.5 year old to have a look after she's finished playing with her dolls ;)
What I have done though, is fallen back onto the available functionality of the API, in particular, the AffineTransform
, which allows you to rotate a Graphics
context (amongst other things).
So, basically, for each segment, I rotate the context, and paint all the dots.
I've also spend some time removing all the "magic" numbers and focused on working with known values (like calculating the actual center of the component based on its width and height)
So, the magic basically happens here...
double delta = 360.0 / (double) nlines;
Graphics2D gCopy = (Graphics2D) g.create();
AffineTransform at = AffineTransform.getRotateInstance(
Math.toRadians(delta),
centerPoint.x,
centerPoint.x);
for (int h = 0; h < nlines; h++) {
for (DoilyPoint j : points) {
gCopy.fillOval(j.getX(), j.getY(), j.getSize(), j.getSize());
}
gCopy.transform(at);
}
gCopy.dispose();
There are number of important concepts to understand
AffineTransform
. This is pretty basic, we supply a anchor point around which the rotation will take place, in this case, the center of the component, and the amount of rotation to apply.AffineTransform
. This is a neat trick to remember, transformations are compounding, so we only need to know the delta amount to change, not the actual angleimport java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.util.Deque;
import java.util.LinkedList;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new Doiles());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class Doiles extends JPanel implements MouseListener, ActionListener, MouseMotionListener {
//global variable declarations
int nlines = 12;//store the number of sector defining lines
int currentovalsize = 10;
Color currentcolour = Color.WHITE;
Deque<DoilyPoint> points = new LinkedList<DoilyPoint>();
Color test[] = {Color.RED,
Color.GREEN,
Color.BLUE, Color.MAGENTA, Color.CYAN};
public Doiles() {
//drawing panel + paint method
JPanel drawingPanel = new JPanel() {
public void paintComponent(Graphics g) {
super.paintComponent(g);
int lineLength = Math.max(getWidth(), getHeight());
Point centerPoint = new Point(getWidth() / 2, getHeight() / 2);
//calculate angle between sectors
double theta = Math.toRadians(360.0 / nlines);
g.setColor(Color.WHITE);
//calculate line coordinates and draw the sector lines
for (int i = 0; i < nlines; i++) {
g.drawLine(centerPoint.x, centerPoint.y,
centerPoint.x + (int) Math.round(lineLength * Math.cos(theta * i)),
centerPoint.y + (int) Math.round(lineLength * Math.sin(theta * i)));
}
double delta = 360.0 / (double) nlines;
Graphics2D gCopy = (Graphics2D) g.create();
AffineTransform at = AffineTransform.getRotateInstance(
Math.toRadians(delta),
centerPoint.x,
centerPoint.x);
for (int h = 0; h < nlines; h++) {
for (DoilyPoint j : points) {
gCopy.fillOval(j.getX(), j.getY(), j.getSize(), j.getSize());
}
gCopy.transform(at);
}
gCopy.dispose();
}
};
drawingPanel.setBackground(Color.BLACK);
drawingPanel.addMouseMotionListener(this);
drawingPanel.addMouseListener(this);
setLayout(new BorderLayout());
add(drawingPanel);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
public void addPoint(int x, int y) {
points.addFirst(new DoilyPoint(currentovalsize, x, y, currentcolour));
repaint();
}
@Override
public void mouseDragged(MouseEvent e) {
addPoint(e.getX(), e.getY());
}
@Override
public void mouseClicked(MouseEvent e) {
// addPoint(e.getX(), e.getY());
}
@Override
public void mousePressed(MouseEvent e) {
}
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
@Override
public void actionPerformed(ActionEvent e) {
}
@Override
public void mouseMoved(MouseEvent e) {
}
}
class DoilyPoint {
private int size;
private int x;
private int y;
private Color colour;
void setSize(int a) {
this.size = a;
}
int getSize() {
return size;
}
void setX(int a) {
this.x = a;
}
int getX() {
return x;
}
void setY(int a) {
this.y = a;
}
int getY() {
return y;
}
void setColor(Color r) {
this.colour = r;
}
Color getColor() {
return colour;
}
public DoilyPoint(int size, int x, int y, Color colour) {
this.size = size;
this.x = x;
this.y = y;
this.colour = colour;
}
}
}
Upvotes: 4