Monday, November 15, 2010

The crazy geometry of basketball courts

I saw an interesting question on dy/dan today about posing basketball-related math questions to students. One of the reader suggestions was to talk open-endedly about the geometry of the court itself, to which Dan suggested writing a program that would draw a court given two mouse clicks to denote the baseline. That sounded right up my alley, so I wrote one.

Before starting to code, I had to learn about the measurements of a basketball court, and I was a little surprised by what I found. First and foremost, every league has its own sizes for every piece of the court, most differing by a foot or less. The biggest discrepancy is with the length of the court, which is 10 feet less for high school than for NCAA and above, or about 2 running steps. Naturally the FIBA league had measurements rounded to meters where all the US leagues are rounded to feet.

A couple other things struck me as odd. First, the backboard is 4 feet from the baseline - inside the court. I always assumed it was flush with the baseline. A second bad assumption of mine was that the three-point line was a conic section, like a parabola or hyperbola. It's not. It's a semi-circle at the top, and straight lines at the bottom, like a horseshoe. The rules imply this shape, but only refer to the three-point line as an arc.

The last organized basketball game I went to was with my daughter and my mentee, and we watched the Ohio State women's team trounce an unranked opponent from the comfort of the AEP suite at the Schottenstein center. As a nod to both of them, and to the AEP mentoring program which scored me the free tickets, all the measurements below are regulation NCAA women's court measurements. And I can assure you those players are capable of shooting from further out than their ridiculously truncated three-point line. And playing with the same ball their male counterparts use: two ounces heavier and one inch more in circumference. I honestly didn't start writing this as a feminist entry; the discrepancies just upset me after I thought about them for a while. Anyway, back to the code.

Java applet to draw a court:


Source code:
package cea.demos.drawCourt;
import java.applet.Applet;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;


public class DrawCourt extends Applet {
  private static final long serialVersionUID = 1L;
  // Paths describing the court lines, and the bottom-most free-throw separators, which are filled in and 1' foot thick
  GeneralPath court, fill;
  Shape courtTransformed, fillTransformed; // The same shapes after an affineTransform is applied to them
  Stroke stroke; // How thick of a line to draw the court with. Most lines are 2" thick (relative to court size)
  // Applet has three draw/input modes: Getting the upper-left point, the upper-right point, and drawing the court
  enum Mode {GET_UL_POINT, GET_UR_POINT, PAINT_COURT}
  Mode mode;
  Point upperLeft;

  public void init() {
    mode = Mode.GET_UL_POINT;
    addMouseListener(new GetClick());

    // Initialize court shape object
    court = new GeneralPath();
    fill = new GeneralPath();

    // Build the court shape. Comments describe what each line is, and the rectangle, ellipse, and arc
    // objects describe the bounding rectangles of the lines
    
    // Out-of-bounds rectangle, 50' by 94'
    court.append(new Rectangle2D.Double(0, 0, 50, 94), false);
    
    // Coaching box markers, 28' from baseline, three feet long, on both sides of court 
    court.append(new Rectangle2D.Double( 0, 28, 3, 0), false);
    court.append(new Rectangle2D.Double(47, 28, 3, 0), false);
    court.append(new Rectangle2D.Double( 0, 66, 3, 0), false);
    court.append(new Rectangle2D.Double(47, 66, 3, 0), false);
    
    // Center-court line, 47' from baseline (94 / 2 = 47), all the way across court (50')
    court.append(new Rectangle2D.Double(0, 47, 50, 0), false);
    
    // Restraining circle and center circle, 2' and 6' radii from middle of center-court line
    court.append(new Ellipse2D.Double(23, 45,  4,  4), false);
    court.append(new Ellipse2D.Double(19, 41, 12, 12), false);
    
    // Back-boards and baskets
    // Back-boards are 4' inside court from baseline, centered, and 6' wide
    court.append(new Rectangle2D.Double(22,  4, 6, 0), false);
    court.append(new Rectangle2D.Double(22, 90, 6, 0), false);
    // Basket are 18" circles mounted 6" from backboard
    court.append(new Ellipse2D.Double(24.25,  4.5, 1.5, 1.5), false);
    court.append(new Ellipse2D.Double(24.25, 88.0, 1.5, 1.5), false);
    
    // NCAA women's 3-point lines are 19'9" from basket's center, until 5'3" from court's side,
    // then straight down to baseline. The rules describing court layouts are ambiguous about this,
    // but this appears to be how they're laid out in reality.
    //  top
    court.append(new Arc2D.Double(5.25, -14.5, 39.5, 39.5, 0, -180, Arc2D.OPEN), false);
    court.append(new Rectangle2D.Double( 5.25, 0, 0, 5.25), false);
    court.append(new Rectangle2D.Double(44.75, 0, 0, 5.25), false);
    // bottom
    court.append(new Arc2D.Double(5.25, 69, 39.5, 39.5, 0, 180, Arc2D.OPEN), false);
    court.append(new Rectangle2D.Double( 5.25, 88.75, 0, 5.25), false);
    court.append(new Rectangle2D.Double(44.75, 88.75, 0, 5.25), false);
    
    // "The Paint", 12' by 19' rectangle, centered and touching baseline
    court.append(new Rectangle2D.Double(19,  0, 12, 19), false);
    court.append(new Rectangle2D.Double(19, 75, 12, 19), false);
    
    // Free-throw circles, the top of The Paint (the free-throw line) is the circle's diameter
    court.append(new Ellipse2D.Double(19, 13, 12, 12), false);
    court.append(new Ellipse2D.Double(19, 69, 12, 12), false);
    
    // Free-throw bottom separators (need to be filled in), are 5' from baseline, 8" wide, 1' long
    // and adjacent to The Paint
    fill.append(new Rectangle2D.Double(19.0d - 2.0d/3, 7, 2.0d/3, 1), false);
    fill.append(new Rectangle2D.Double(31, 7, 2.0d/3, 1), false);
    fill.append(new Rectangle2D.Double(19.0d - 2.0d/3, 86, 2.0d/3, 1), false);
    fill.append(new Rectangle2D.Double(31, 86, 2.0d/3, 1), false);
    
    // Other free-throw separators, 3' between edges, also 8" wide and adjacent to The Paint,
    // but only 2" long
    // top-left
    court.append(new Rectangle2D.Double(19.0d - 2.0d/3, 11, 2.0d/3, 0), false);
    court.append(new Rectangle2D.Double(19.0d - 2.0d/3, 14.0d + 1.0d/6, 2.0d/3, 0), false);
    court.append(new Rectangle2D.Double(19.0d - 2.0d/3, 17.0d + 1.0d/3, 2.0d/3, 0), false);
    // top-right
    court.append(new Rectangle2D.Double(31, 11, 2.0d/3, 0), false);
    court.append(new Rectangle2D.Double(31, 14.0d + 1.0d/6, 2.0d/3, 0), false);
    court.append(new Rectangle2D.Double(31, 17.0d + 1.0d/3, 2.0d/3, 0), false);
    // bottom-left
    court.append(new Rectangle2D.Double(19.0d - 2.0d/3, 83, 2.0d/3, 0), false);
    court.append(new Rectangle2D.Double(19.0d - 2.0d/3, 80.0d - 1.0d/6, 2.0d/3, 0), false);
    court.append(new Rectangle2D.Double(19.0d - 2.0d/3, 77.0d - 1.0d/3, 2.0d/3, 0), false);
    // bottom-right
    court.append(new Rectangle2D.Double(31, 83, 2.0d/3, 0), false);
    court.append(new Rectangle2D.Double(31, 80.0d - 1.0d/6, 2.0d/3, 0), false);
    court.append(new Rectangle2D.Double(31, 77.0d - 1.0d/3, 2.0d/3, 0), false);
  }
  
  // This method creates transformed shapes of the court shape archetypes.
  public void scaleCourt(Point left, Point right) {
    double radians;
    if (left.x == right.x) radians = left.y < right.y ? Math.PI/2 : -Math.PI/2; // Avoid divide by 0 error
    else radians = Math.atan((float)(right.y - left.y)/(right.x - left.x));
    if (right.x < left.x)radians += Math.PI; // Fix angle if in quadrants 2 or 3

    /* "factor" bears some explanation. The default size of the court shape is 1 pixel per foot. Two points
     * on the screen are clicked, representing corners of the baseline: 50' across. So, the factor to scale the shape
     * will be the real distance between the corner points divided by the default size of 50.
     */
    float factor = (float) (left.distance(right)/50);
    stroke = new BasicStroke(factor * 2.0f / 12); // Default stroke = 2 inches relative to court size
    AffineTransform tx = new AffineTransform();
    tx.setToScale(factor, factor);
    tx.rotate(radians);
    courtTransformed = tx.createTransformedShape(court);
    fillTransformed = tx.createTransformedShape(fill);
  }
  
  public void paint(Graphics g1) {
    Graphics2D g = (Graphics2D)g1;
    g.setColor(Color.BLACK);
    switch(mode) {
    case GET_UL_POINT:
      g.drawString("Click where the upper-left corner of the court will be.", 50, 50);
      break;
    case GET_UR_POINT:
      g.fillOval(upperLeft.x - 4, upperLeft.y - 4, 8, 8);
      g.drawString("Now click where the upper-right corner should be", 50, 50);
      break;
    case PAINT_COURT:
      g.drawString("Click again to start over", 50, 50);
      g.setStroke(stroke);
      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      g.translate(upperLeft.x, upperLeft.y);
      g.draw(courtTransformed);
      g.fill(fillTransformed);
    }
  }

  class GetClick extends MouseAdapter {
    @Override
    public void mouseClicked(MouseEvent e) {
      switch (mode) {
      case GET_UL_POINT:
        upperLeft = e.getPoint();
        mode = Mode.GET_UR_POINT;
        repaint();
        break;
      case GET_UR_POINT:
        mode = Mode.PAINT_COURT;
        scaleCourt(upperLeft, e.getPoint());
        repaint();
        break;
      case PAINT_COURT:
        mode = Mode.GET_UL_POINT;
        repaint();
      }
    }
  }
}

2 comments:

  1. Nice, thank you =)! Do you have this code for an application (non-applet) :)?

    ReplyDelete
  2. Thanks. No, I don't have this as an application. If you extend JFrame instead of Applet and implement Runnable, I don't imagine it would take too much jury-rigging to get the rest working.

    ReplyDelete