Bouncing Balls

Nicholas Duchon: Mar 9, 2019

Key Concepts:

  • Bouncing balls passing through each other
  • Extending the JButton class - NDCB
  • Using Consumer function


Image:


Features:


Code:



// File: Main.java
// Date: Jul 29, 2010
// Author: Nicholas Duchon
// Purpose: physics experiments - bouncing balls, 2 and 3 D eventually
// History:
//   Mar 09, 2019 - moved from JApplet to JFrame, various updates

import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JSlider;
import javax.swing.SwingConstants;
import javax.swing.Timer;
import javax.swing.event.ChangeEvent;

import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.Color;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

import java.util.Stack;
import java.util.EmptyStackException;
import java.util.Hashtable;
import java.util.Dictionary;
import java.util.Random;

public class Main extends JFrame implements BallOwner {
    public static final long serialVersionUID = 123; // ND: junk
    final static private String version = "0.0.2";
    final static private String progName = "Bouncing Balls Processor";
    final static private String copyrightNotice = progName + " - Copyright 2003-2019 by Nicholas Duchon";
    final static private int    WINDOWWIDTH = 600;
    final static private int    WINDOWHEIGHT = 400;
    final static private double INITIALSPEED = 12; // slider units:0 - 2*
    final static private Random rn = new Random ();
   
    JSlider speedS;
    JPanel box = new JPanel ();
    Stack <Ball>    balls   = new Stack <Ball> ();
    Timer tm;
    boolean showNumberFlag = false, gravityFlag = false;
    double gravity = 0, speed = 1; // in pixels per update of timer

    JButton [] jbList = {
            new NDCB ("Add"    , Color.green, e -> newBall()      ),
            new NDCB ("Remove" , Color.red  , e -> removeBall()   ),
            new NDCB ("Shake"  , Color.cyan , e -> shakeBalls()   ),
            new NDCB ("Show"   , Color.red  , e -> showTags()     ),
            new NDCB ("Gravity", Color.red  , e -> gravityToggle())
    } ;
   
    static {
        try {
              javax.swing.UIManager.setLookAndFeel( javax.swing.UIManager.getCrossPlatformLookAndFeelClassName() );
            } catch (Exception e) {
              e.printStackTrace();
            }
    } // static initializer - this seems to not work in constructor because jbList as instance vars??
   
    public Main () {
        JOptionPane.showMessageDialog (null, copyrightNotice + "\nVersion: " + version);
       
        setTitle (progName + ": " + version);
        setSize (WINDOWWIDTH, WINDOWHEIGHT);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setVisible(true);

        add(box, BorderLayout.CENTER);
        box.setBackground(Color.black);
        box.setLayout(null);
       
        JPanel pControls = new JPanel ();
        pControls.setLayout(new GridLayout (0, 5));
       
        for (JButton jb: jbList) pControls.add(jb);
        add (pControls, BorderLayout.SOUTH);
       
        tm = new Timer (10, e -> moveBalls());
        tm.start();

        int isd = (int)INITIALSPEED / 2;
        speedS = new JSlider (JSlider.HORIZONTAL, 0, 4*isd, 2*isd);
        speedS.setBackground(Color.yellow);
        speedS.setMajorTickSpacing(5);
        speedS.setMinorTickSpacing(1);
        speedS.setPaintLabels(true);
        speedS.setPaintTicks(true);
        speedS.setSnapToTicks(true);
       
        // add labels to the slider
        Dictionary <Integer, JLabel> sliderLabels = new Hashtable <Integer, JLabel> ();
        sliderLabels.put (    0, new JLabel("0"));
        sliderLabels.put (  isd, new JLabel("0.25"));
        sliderLabels.put (2*isd, new JLabel("1"));
        sliderLabels.put (3*isd, new JLabel("2"));
        sliderLabels.put (4*isd, new JLabel("4"));
        speedS.setLabelTable(sliderLabels);
       
        speedS.addChangeListener(this::setSpeed);
       
        pControls.add (new JLabel ("Speed", SwingConstants.CENTER));
        pControls.add (speedS);
       
        validate ();
       
   
} // end constructor
   
    void setSpeed (ChangeEvent e) {
        double s = ((JSlider)e.getSource()).getValue() / INITIALSPEED;
        speed = s*s;
   
} // end method setSpeed
   
    void moveBalls  () {for (Ball x: balls) x.move();
}
    void shakeBalls () {for (Ball x: balls) x.shake(rn);
}
   
    public void newBall () {
        Ball x = new Ball (rn, box, this);
        balls.add(x);
        box.add(x);
        repaint ();
   
} // end method newBall
   
    public void showTags () {
        JButton me = jbList[3];
        if (showNumberFlag) {
            showNumberFlag = false;
            me.setText("Show");
            me.setBackground(Color.red);
           
        } else {
            showNumberFlag = true;
            me.setText("Hide");
            me.setBackground(Color.green);
        } // go?
        repaint ();
   
} // end method showTags
   
    public void gravityToggle () {
        JButton me = jbList[4];
        if (gravityFlag) {
            gravityFlag = false;
            gravity = 0;
            me.setText("Gravity");
            me.setBackground(Color.red);
           
        } else {
            gravityFlag = true;
            gravity = 0.1;
            me.setText("No Gravity");
            me.setBackground(Color.green);
        } // go?
        repaint ();
   
} // end method gravityToggle
   
    public void removeBall () {
        try {
            Ball x = balls.pop();
            box.remove(x);
            Ball.count--;
            repaint ();
        }
        catch (EmptyStackException ex) {}
   
} // end method removeBall
   
    public boolean showLabel  () {return showNumberFlag;
}
    public double  getGravity () {return gravity;
}
    public double  getSpeed   () {return speed;
}
   
    public static void main(String[] args) {
        new Main ();
   
} // end main

} // end class Main

class NDCB extends JButton implements ActionListener {
    public static final long serialVersionUID = 123; // ND: junk
    java.util.function.Consumer<ActionEvent> func;
   
    public NDCB (String st, Color c, java.util.function.Consumer<ActionEvent> f) {
        super (st);
        setBackground (c);
        func = f;
        addActionListener (this);
   
} // end constructor

    public void actionPerformed (ActionEvent e) {
        func.accept(e);
    } // end method actionPerformed
   
} // end class NDCB


File: BallOwner.java

// File: BallOwner.java
// Date: Jul 30, 2010
// Author: Nicholas Duchon
// Purpose: allow communications between Balls and their owner

public interface BallOwner {
    public boolean showLabel  ();
    public double  getGravity ();
    public double  getSpeed   ();
} // end BallOwner interface

File: Ball.java

// File: Ball.java
// Date: Jul 27, 2010
// Author: Nicholas Duchon
// Purpose: physics of balls

import javax.swing.JComponent;
import java.awt.Graphics;
import java.awt.Color;
import java.awt.Rectangle;
import java.util.Random;

public class Ball extends JComponent {
    public static final long serialVersionUID = 123; // ND: junk
    final static double MAXV = 5;
    final static int MAXD = 40, MIND = 10;
    static int count = 0;
   
    BallOwner owner = null;
    int index;
    String indexString;
    double xr, yr, dx, dy, dz;
    int xString, yString;
    Color color = Color.red;
   
    public Ball (Random rn, JComponent pc, BallOwner bo) {
        Rectangle bx = pc.getBounds();
        int rad = rn.nextInt(MAXD) + MIND;
        xr = rn.nextDouble() * (bx.width - bx.x - rad)+ bx.x;
        yr = rn.nextDouble() * (bx.height - bx.y - rad)+ bx.y;
        shake (rn);
        color = Color.getHSBColor(rn.nextFloat(), 1, 1);
        this.setBounds((int)(xr+0.5), (int)(yr+0.5), rad, rad);
        commonInit(bo);
   
} // create random Ball
   
    public Ball (int ix, int iy, int iw, int ih, double idx, double idy, BallOwner bo) {
        this.setBounds(ix, iy, iw, ih);
        xr = ix; yr = iy;
        dx = idx; dy = idy;
//        System.out.printf ("init: Bounds: %d, %d, %d, %d\n", getX(), getY(), getWidth(), getHeight());
        commonInit (bo);
   
} // end 4-int constructor
   
    public void commonInit (BallOwner bo) {
        owner = bo;
        this.setForeground(color);
        index = count++;
        indexString = String.format("%d", index);
        xString = this.getWidth() / 2 - 5;
        yString = this.getHeight() / 2 + 5;
   
} // end method commonInit
   
    public void move () {
        Rectangle bx = this.getParent().getBounds();
        // System.out.println ("Parent: " + bx);
        dy = dy + owner.getGravity();
        if (xr < bx.x && dx < 0) dx = -dx;
        else if (xr + getWidth() > bx.x + bx.width && dx > 0) dx = -dx;
        if (yr < bx.y && dy < 0) dy = -dy;
        else if (yr + getHeight() > bx.y + bx.height && dy > 0) dy = -dy;
        xr = xr + dx*owner.getSpeed();
        yr = yr + dy*owner.getSpeed();
        this.setLocation((int)(xr+0.5), (int)(yr+0.5));
   
} // end method move
   
    public void shake (Random rn) {
        dx = rn.nextDouble() * MAXV * 2 - MAXV;
        dy = rn.nextDouble() * MAXV * 2 - MAXV;
   
} // end method shake
   
    public void reflectX () {
        dx = -dx;
   
} // end method reflectX   

    public void reflectY () {
        dy = -dy;
   
} // end method reflectY
   
    public void paint (Graphics g) {
        g.fillOval(0, 0, getWidth(), getHeight()); // drawing relative to bounds
        if (owner.showLabel()) {
            g.setColor(Color.white);
            g.drawString(indexString, xString, yString);
            g.setColor(color);
        }
        // System.out.printf ("Bounds: %d, %d, %d, %d\n", getX(), getY(), getWidth(), getHeight());
   
} // end method paint
   
    public String toString () {
        return String.format("%d, %.2f, %.2f, %.2f, %.2f", count, xr, yr, dx, dy);
   
} // end toString method

} // end class Ball



Extra code from earlier versions:

//Stack <JButton> buttons = new Stack <JButton> ();

//speedS.addChangeListener(new ChangeListener (){
//public void stateChanged (ChangeEvent e) {
//    double s = ((JSlider)e.getSource()).getValue() / INITIALSPEED;
//    speed = s*s;
//}
//});

//for (JButton jb: buttons) pControls.add(jb);

//buttons.add(jbAdd);     // 0
//buttons.add(jbRemove);  // 1
//buttons.add(jbShake);   // 2
//buttons.add(jbShowN);   // 3
//buttons.add(jbGravity); // 4
//buttons.add(jbx);
//JButton jbx = new NDCB ("Test", Color.pink, e -> newBall());

//JButton jbAdd     = new JButton ("Add");
//JButton jbRemove  = new JButton ("Remove");
//JButton jbShow    = new JButton ("Show");
//JButton jbShake   = new JButton ("Shake");
//JButton jbShowN   = new JButton ("Show");
//JButton jbGravity = new JButton ("Gravity");
//jbAdd.setBackground(Color.green);
//jbRemove.setBackground(Color.red);
//jbShow.setBackground(Color.red);
//jbShowN.setBackground(Color.red);
//jbGravity.setBackground(Color.red);

//buttons.get(0).addActionListener(e -> newBall());
//buttons.get(1).addActionListener(e -> removeBall());
//buttons.get(2).addActionListener(e -> shakeBalls());
//buttons.get(3).addActionListener(e -> showTags());
//buttons.get(4).addActionListener(e -> gravityToggle());

//buttons.get(0).addActionListener(new ActionListener () {
//public void actionPerformed (ActionEvent e) {
//    newBall ();
//    repaint();
//}
//}); // end actionListener "Add"

//buttons.get(1).addActionListener(new ActionListener () {
//public void actionPerformed (ActionEvent e) {
//    removeBall ();
//    repaint();
//}
//}); // end actionListener "Remove"

//buttons.get(2).addActionListener(new ActionListener () {
//    public void actionPerformed (ActionEvent e) {
//        for (Ball b: balls) b.shake(rn);
//    }
//}); // end actionListener "Shake"

//new ActionListener () {
//public void actionPerformed (ActionEvent e) {
//JButton me = buttons.get(3);
//if (showNumberFlag) {
//    showNumberFlag = false;
//    me.setText("Show");
//    me.setBackground(Color.red);
//   
//} else {
//    showNumberFlag = true;
//    me.setText("Hide");
//    me.setBackground(Color.green);
//} // go?
//repaint ();
//} // end actionPerformed method
//}); // end actionListener "Start/Stop"

//buttons.get(4).addActionListener(new ActionListener () {
//    public void actionPerformed (ActionEvent e) {
//        JButton me = buttons.get(4);
//        if (gravityFlag) {
//            gravityFlag = false;
//            gravity = 0;
//            me.setText("Gravity");
//            me.setBackground(Color.red);
//           
//        } else {
//            gravityFlag = true;
//            gravity = 0.1;
//            me.setText("No Gravity");
//            me.setBackground(Color.green);
//        } // go?
//        repaint ();
//    } // end actionPerformed method
//}); // end actionListener "Start/Stop"

//tm = new Timer (10, new ActionListener () {
//public void actionPerformed (ActionEvent e) {
//} // end method action performed
//} // end anonymous class definition
//); // end parameter for Timer Constructor

//JFrame j = new JFrame("MyExpression Processor - Copyight Nicholas Duchon: " + version);
//j.add(a);
//j.setSize (WINDOWWIDTH, WINDOWHEIGHT);
//j.setLocationRelativeTo(null);
//a.init();
//a.start();
//j.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//j.setVisible(true);

//buttons.get(0).addActionListener(new ActionListener () {
//public void actionPerformed (ActionEvent e) {
//    for (Ball x: balls) x.move();
//}
//}); // end actionListener "move"
//
//buttons.get(1).addActionListener(new ActionListener () {
//public void actionPerformed (ActionEvent e) {
//    for (Ball x: balls) x.reflectX();
//}
//}); // end actionListener "reflect X"
//
//buttons.get(2).addActionListener(new ActionListener () {
//public void actionPerformed (ActionEvent e) {
//    for (Ball x: balls) x.reflectY();
//}
//}); // end actionListener "reflect Y"
//
//buttons.get(4).addActionListener(new ActionListener () {
//public void actionPerformed (ActionEvent e) {
//    tm.stop();
//}
//}); // end actionListener "Stop"

//buttons.get(2).addActionListener(new ActionListener () {
//public void actionPerformed (ActionEvent e) {
//    JButton me = buttons.get(2);
//    if (goFlag) {
//        tm.stop();
//        goFlag = false;
//        me.setText("Start");
//        me.setBackground(Color.red);
//       
//    } else {
//        tm.start();
//        goFlag = true;
//        me.setText("Stop");
//        me.setBackground(Color.green);
//    } // go?
//} // end actionPerformed method
//}); // end actionListener "Start/Stop"

//balls.add(new Ball (20, 20, 50, 50, 1, 2));
//for (Ball x: balls) box.add(x);





End.