imulsion
imulsion

Reputation: 9040

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?

Upvotes: 2

Views: 227

Answers (2)

Preston Garno
Preston Garno

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:

  1. Space
  2. Time

I'll provide an example of how to do it both ways.

Space

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:

Time

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

MadProgrammer
MadProgrammer

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.

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)

Upvotes: 4

Related Questions