Search code examples
javaswingpaintcomponentmouselistener

Bezier curve not showing in frame


The user has to pick 4 points by clicking anywhere in the frame, and then the program is supposed to draw a Bezier curve. I've also included a method that draws small circles where the user click so it's easier to see.

I'm not getting any errors, but the curve is just not showing. Obviously, I'm missing something, but I can't figure out what.

Code:

public class Splines {
    public Splines(){
        JFrame frame = new JFrame("Bezier curves");
        frame.add(new draw());
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public class draw extends JPanel implements MouseListener{
        Point[] controlPoints = new Point[100];
        ArrayList<Point> punkter = new ArrayList<>();   
        int pSize = punkter.size();

        public draw(){
            addMouseListener(this);
        }




        @Override
        public void mousePressed(MouseEvent e) {
            if (pSize==4) drawBezier(pSize,4,getGraphics());

            drawPoint(e);   
            pSize++;

        }

        //Method drawing points to visualize the control points
        public void drawPoint(MouseEvent evt){
            Graphics g = getGraphics();
            Graphics2D g2d = (Graphics2D) g;
            punkter.add(new Point(evt.getX(), evt.getY()));
            g2d.setColor(Color.red);
            g2d.fillOval(punkter.get(pSize).x, punkter.get(pSize).y, 5, 5);

            controlPoints[pSize] = punkter.get(pSize);
        }


        public void drawBezier(int i, int n, Graphics g) {
            int j;
            double t, delta;
            Point curvePoints[] = new Point[n + 1];
            delta = 1.0 / n;

            for (j = 0; j <= n; j++) {
                t = j * delta;
                curvePoints[j] = new Point();

                curvePoints[j].x = (int) Math.round(controlPoints[i - 3].x * (1.0 - t) * (1.0 - t) * (1.0 - t)
                                                  + controlPoints[i - 2].x * 3.0 * t * (1.0 - t) * (1.0 - t)
                                                  + controlPoints[i - 1].x * 3.0 * t * t * (1.0 - t) 
                                                  + controlPoints[i].x * t * t * t);

                curvePoints[j].y = (int) Math.round(controlPoints[i - 3].y * (1.0 - t) * (1.0 - t) * (1.0 - t)
                                                  + controlPoints[i - 2].y * 3.0 * t * (1.0 - t) * (1.0 - t)
                                                  + controlPoints[i - 1].y * 3.0 * t * t * (1.0 - t) 
                                                  + controlPoints[i].y * t * t * t);
            }

            g.setColor(Color.red);
            for (j = 0; j < n; j++)
                g.drawLine(curvePoints[j].x, curvePoints[j].y, curvePoints[j + 1].x, curvePoints[j + 1].y);

        } // End drawBezier    

        @Override
        public void mouseClicked(MouseEvent e) {
            // TODO Auto-generated method stub

        }
        @Override
        public void mouseEntered(MouseEvent e) {
            // TODO Auto-generated method stub

        }
        @Override
        public void mouseExited(MouseEvent e) {
            // TODO Auto-generated method stub

        }
        @Override
        public void mouseReleased(MouseEvent e) {
            // TODO Auto-generated method stub

        }




        @Override
        public Dimension getPreferredSize() {
            return new Dimension(600, 400);
        }
    }//End draw class





    public static void main(String[] args) {
        new Splines();
    }//End main method

}//End Spline class

Solution

  • Although I strongly advise you to take into consideration what both MadProgrammer and Petter Friberg pointed out about painting in AWT and Swing, your immediate problem here is quite trivial.

    Specifically, it lies within the drawBezier() method which raises a NullPointerException at the very first time it tries to access the controlPoints array.

    This, of course, happens because you are trying to access controlPoints[i] when, in fact, i has a value of 4 and controlPoints is zero-based which means that you are referencing a, practically, non-existent element ( controlPoints[4] is null ). See more on array initialization in Thorbjørn Ravn Andersen's answer, here.

    The solution should be obvious to you by now:

    curvePoints[j].x = (int) Math.round(controlPoints[i - 4].x * (1.0 - t) * (1.0 - t) * (1.0 - t)
                                      + controlPoints[i - 3].x * 3.0 * t * (1.0 - t) * (1.0 - t)
                                      + controlPoints[i - 2].x * 3.0 * t * t * (1.0 - t) 
                                      + controlPoints[i - 1].x * t * t * t);
    
    curvePoints[j].y = (int) Math.round(controlPoints[i - 4].y * (1.0 - t) * (1.0 - t) * (1.0 - t)
                                      + controlPoints[i - 3].y * 3.0 * t * (1.0 - t) * (1.0 - t)
                                      + controlPoints[i - 2].y * 3.0 * t * t * (1.0 - t) 
                                      + controlPoints[i - 1].y * t * t * t);
    

    which delivers this:

    enter image description here


    I also made a small edit in mousePressed(), so that it returns after the Bezier curve is drawn:

    @Override
    public void mousePressed(MouseEvent e) {
        if (pSize==4) {
            drawBezier(pSize,4,getGraphics());
            return;
        }
        drawPoint(e);   
        pSize++;
    }
    

    If you do this edit, then try this:

    After you click 5 times the Bezier curve is drawn. Now, minimize the window and restore it. You will see that it is blank. However, if you click once more inside you will see that the curve reappears (only the curve; not the control points). Why does this happen?

    If you can answer this then you are getting one step closer to understanding some of the issues mentioned in the comments and Petter Friberg's answer.


    Update / Appendix - Giving Persistence

    The whole idea is to find a way to make your drawing persistent to arbitrary user actions, eg minimizing, resizing etc. As Petter Friberg pointed out, the way to do this is to override the paintComponent() method.

    How to do this? Well, actually it's quite simple:

    First remove your drawPoint() method completely. It's functionality will be split among mousePressed() and paintComponent(), like this:

    @Override
    public void mousePressed(MouseEvent e) {
        if (pSize<4){
            punkter.add(new Point(e.getX(), e.getY()));
            controlPoints[pSize] = punkter.get(pSize);
            pSize++;
            repaint();
        } else if (pSize==4){
            // do other stuff, for example reset everything and start over
    
            //pSize = 0;
            //punkter.clear();
            //for (int i= 0; i < controlPoints.length; i++){
            //    controlPoints[i]= null;
            //}
            //repaint();
            //System.out.println("Reset");
        }
    }
    

    and this:

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;
        g2d.setColor(Color.red);
        for (int i=0; i<pSize; i++){
            g2d.fillOval(punkter.get(i).x, punkter.get(i).y, 5, 5);   
        }
        if (pSize==4) {
            drawBezier(pSize,4,g);
        }
    }
    

    One thing worth pointing out here, is that you still need to invoke the superclass's paintComponent() method and this is achieved in this line : super.paintComponent(g);

    Also, getGraphics() is no longer valid (if it ever was) as an argument to drawBezier(); instead, the g Graphics object is used.

    By now the application has the persistence it lacked before.

    Notice that there is nowhere in the code a direct call to paintComponent(). Instead, it is invoked by the graphics subsystem when you call repaint(), or the system decides that it is time to redraw. One way to see this is to comment out the call to repaint(). Now there is not a single call to any drawing function. Does that mean that nothing gets drawn? Or, maybe, you can indirectly invoke the drawing by performing some, seemingly irrelevant, actions?