imulsion imulsion - 9 months ago 29
Java Question

Strange behaviour from JFrame point drawing

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):
enter image description here

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?

Answer Source

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.

Pretty Patterns

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)

The Magic

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

  • First, we make a copy of the graphics context (this just copies the current state), this is important, as we don't want to mess with the current context, as this gets shared with other components, and undoing is a pain
  • Next we create a rotational 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.
  • Next for each segment, we paint all the dots
  • We then transform the copied context using the 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 angle

Runnable Example

import 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;
        }
    }
}

Suggestions...

  • When I was testing this, I reduced the number of segments down to two and three in order to make it a little simpler
  • I used mouse clicked rather than mouse dragged so I could better control the creation of the dots and see what was actually going
  • I set up a separate color for each segment, so I could see where the dots were actually getting painted
  • Given the frequency that the question relating to this and similar problems is occurring, please share this information with the other students in your class, because it's becoming tiresome repeating the basic some solution(s)