Example of TSimplePantograph class to map (x, y) in feet to pixel (i, j)

Purpose
The purpose of this project is to show how to map real-world coordinates to pixel coordinates.  In addition, this particular example shows how to draw a diagram of a basketball court and how to determine if a given point on the court would be a 2-point or 3-point shot against a specified goal.

Materials and Equipment

Software Requirements
Windows 95/98/2000
Delphi 3/4/5 (to recompile)

Hardware Requirements
VGA display monitor

Procedure

1. From Windows Explorer, double-click on the Basketball.Exe icon to start the program.
2. With the ComboBox at the upper-left of the screen, choose between "High School / College" and "NBA".
3. With the default "Time Out" ComboBox selected at the upper-right of the screen, move the cursor around the image to see both the (x, y) coordinates in feet and the (i, j) pixel coordinates.
4. From the ComboBox selected at the upper-right, select "Shoot on Left Goal" or "Shoot on Right Goal".  Move the cursor around the image to see one of these two messages:
• Out of bounds
• Distance to goal xx.x feet -> {TWO | THREE} points

Discussion

Consult various web sites, such as the following, to see maps and specifications of basketball courts:

A college or high school court is 84 feet long by 50 feet wide.  An NBA court is 94 feet long by 50 feet wide.  So let's define "world coordinates" for this problem to be an area with coordinates from -50 to +50 feet from left-to-right, and from -30 to +30 feet from bottom to top.  These world coordinates will have an origin an the center of the basketball court, and will have the "normal" quadrants from mathematics.

Before introducing the TSimplePantograph class, let's review the mathematics needed for this mapping of (x, y) real coordinates to (i, j) pixel coordinates.

Given:
i integer pixel coordinate (increasing left-to-right)
x real world coordinate (increasing left-to-right)

j integer pixel coordinate (increasing top-to-bottom)
y real world coordinate (increasing bottom-to-top)

These ratios are maintained:
(x - xMin) / (xMax - xMin)  =  (i - iMin) / (iMax - iMin)

(y - yMax) / (yMin - yMax) = (j - jMin) / (jMax - jMin)

Rearrangement of these ratios yield the following expressions:

Map (i, j) Pixel Coordinates to (x,y) World Coordinates
x = xMin + (xMax - xMin)(i - iMin) / (iMax - iMin)
y = yMax + (yMin - yMax)(j - jMin) / (jMax - jMin)

By inspection:
(iMin, jMin) maps to (xMin, yMax)
(iMax, jMax) maps to (xMax, yMin)

Map (x,y) World Coordinates to (i, j) Pixel Coordinates
i = iMin + (iMax - iMin)(x - xMin) / (xMax - xMin)
j = jMin + (jMax - jMin)(y - yMax) / (yMin - yMax)

All of the above equations simplify somewhat when iMin=0 or jMin=0, which is often the case.

The TSimplePantograph class is used to make the mapping to/from the real world and pixel coordinates.

 MapWorldToPixel.Pas ```// Define "Real" analogs to TPoint and TRect TYPE TReal = Double;``` ``` TRealPoint = RECORD x: TReal; y: TReal END;``` ``` TRealRect = RECORD CASE Integer OF 0: (Left, Top, Right, Bottom: TReal); 1: (TopLeft, BottomRight: TRealPoint) END;``` ``` TSimplePantograph = CLASS(TObject) PRIVATE FCanvas : TCanvas;``` ``` FPixelRect: TRect; FRealRect : TRealRect;``` ``` FxDelta : TReal; FyDelta : TReal; FiDelta : INTEGER; FjDelta : INTEGER;``` ``` FiDeltaOverxDelta: TReal; FjDeltaOveryDelta: TReal;``` ``` FxDeltaOveriDelta: TReal; FyDeltaOverjDelta: TReal; ``` ``` PUBLIC CONSTRUCTOR Create(Canvas: TCanvas; PixelRect: TRect; RealRect: TRealRect);``` ``` // Could make these functions but don't always want to use .x and .y // to access fields. PROCEDURE MapRealToPixel(CONST x,y: TReal; VAR i,j: INTEGER); PROCEDURE MapPixelToReal(CONST i,j: INTEGER; VAR x,y: TReal);``` ``` PROCEDURE MoveTo(CONST x,y: TReal); PROCEDURE LineTo(CONST x,y: TReal); PROCEDURE Ellipse(CONST x1,y1, x2,y2: TReal); PROCEDURE Arc(CONST x1,y1, x2,y2, x3,y3, x4,y4: TReal);``` ``` PROPERTY Canvas: TCanvas READ FCanvas; END;``` ``` FUNCTION RealPoint(CONST aX, aY: DOUBLE): TRealPoint; FUNCTION RealRect(CONST aLeft, aTop, aRight, aBottom: DOUBLE): TRealRect;```

The SimplePantograph constructor connects a specified canvas with the two rectangular areas:  the array of pixels, and the world coordinates that define the basketball court..

 ScreenBasketball.PAS ```... Bitmap.Width := ImageCourt.Width; Bitmap.Height := ImageCourt.Height; Bitmap.PixelFormat := pf24bit; ... // Origin of area is the center of the basketball court. "Normal" // math quadrants are used ("y" dimension is flipped). Pantograph := TSimplePantograph.Create( Bitmap.Canvas, Rect(0,0,Bitmap.Width,Bitmap.Height), RealRect(-50,30, 50,-30 {ft}) ); ...```

Drawing the basketball court was broken into several local routines in the DrawCourt method in the ScreenBasketball unit:

 ``` DrawOutline; DrawCenterCourt; DrawHashLines; ... DrawFoulLanes; ... DrawThreePointLines;```

Instead of using the MoveTo/LineTo Canvas methods that operate with pixel coordinates, similar methods of the TSimplePantograph can be used to draw directly in "world coordinates."  For example, DrawOutline shows the outline of the basketball court:

 ``` PROCEDURE DrawOutline; BEGIN // Outline of court Pantograph.MoveTo(-HalfLength, HalfWidth); Pantograph.LineTo( HalfLength, HalfWidth); Pantograph.LineTo( HalfLength,-HalfWidth); Pantograph.LineTo(-HalfLength,-HalfWidth); Pantograph.LineTo(-HalfLength, HalfWidth);``` ``` // Save this rect to determine if shot is "in bounds" RectCourt := RealRect(-HalfLength,HalfWidth, HalfLength,-HalfWidth); END {DrawOutline};```

The most difficult part of the court to draw is the 3-point line, especially in a way that can be used to draw both the college court and the NBA court.  In both cases, the 3-point line is an arc of a circle with a specified radius.  This arc intersects two lines that are parallel to the sides of the court.  These lines parallel to the sides of the court are also at a specified location and length.  The following shows how draw the 3-point line around the "right" goal in the diagram.  Similar code is used to draw the line for the "left" goal.

 ```PROCEDURE DrawThreePointLines; VAR xCenter: Double; yCenter: Double; BEGIN``` ``` // Three Point Line: Right side xCenter := BaselineRight - BackboardToBaseLine - GoalCentertoBackBoard; yCenter := 0.0; GoalRight := RealPoint(xCenter, yCenter);``` ``` Pantograph.Arc(xCenter-ThreePointRadius, yCenter+ThreePointRadius,``` ``` xCenter+ThreePointRadius, yCenter-ThreePointRadius,``` ``` BaselineRight - ThreePointStraight, ThreePointSide,``` ``` BaselineRight - ThreePointStraight, -ThreePointSide);``` ``` Pantograph.MoveTo(BaselineRight, ThreePointSide); Pantograph.LineTo(BaselineRight - ThreePointStraight, ThreePointSide); Pantograph.MoveTo(BaselineRight, -ThreePointSide); Pantograph.LineTo(BaselineRight - ThreePointStraight, -ThreePointSide);``` ``` // Save RectRight for 2/3-point determination RectRight := RealRect(BaselineRight - ThreePointStraight, ThreePointSide, BaselineRight, -ThreePointSide);``` `...`

The (xCenter, yCenter) point above is the center of the basket rim projected onto the floor.

The ImageCourtMouseMove method is called to display various data in the TLabel at the lower left of the screen.  As shown below, the Pantograph.MapPixelToReal method maps the pixel (X, Y) to world coordinates (xFeet, yFeet).  [I usually use (I, J) for a pixel coordinate but here I must deal with Delphi's convention in the automatic definition of this function's header.]  The case "0" of the CombBoxShot is for the "Time Out" state.  This default state simply shows both sets of coordinates.

 ```procedure TFormBasketballCourt.ImageCourtMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); VAR Distance: Double; xFeet : Double; yFeet : Double; s : STRING;``` ``` FUNCTION InRealRect(CONST RealRect: TRealRect; CONST X, Y: Double): BOOLEAN; BEGIN RESULT := (X >= RealRect.Left) AND (X <= RealRect.Right) AND (Y <= RealRect.Top) AND (Y >= RealRect.Bottom)``` ` END {InRealrect};` ```begin Pantograph.MapPixelToReal(X,Y, xFeet,yFeet);``` ` s := '';` ``` Case ComboBoxShot.ItemIndex OF 0: BEGIN // "Time out" s := 'Pixel(' + IntToStr(X) + ', ' + IntToStr(Y) + ') Feet(' + Format('%.1f, %.1f)', [xFeet, yFeet]); END;``` ``` 1: BEGIN // Shot on left goal IF InRealRect(RectCourt, xFeet,yFeet) THEN BEGIN Distance := SQRT( SQR(xFeet-GoalLeft.X) + SQR(yFeet-GoalLeft.Y) ); s := Format('Distance to goal %.1f feet', [Distance]);``` ``` IF InRealRect(RectLeft, xFeet,yFeet) OR // Inside rectangle ((xFeet > RectLeft.Right) AND (Distance < ThreePointRadius) ) // Inside arc THEN s := s + ' -> TWO points' ELSE s := s + ' -> THREE points' END ELSE s := 'Out of bounds' END;``` `...`

Case "1" above shows the logic for the "Shoot on Left Goal" ComboBox selection.  The first check is to see if the point is within the RealRect that defines the outline of the basketball court.  If the point is outside the court, "Out of bounds" is displayed.  For an "in bounds" location, the next IF statement determines whether a shot scores two or three points.

If you study the floor diagram, the union of two areas define where two points are scored.  The remaining areas are where three points are scored.  The two-point area is the union of a rectangle and a "slice" of a circle.

The rectangle is defined by the lines parallel to the sides of the court.  The InRealRect function above determines if a mouse location is in this area.  In addition to this rectangular area, the sector check is a bit more complicated.  A simple distance calculation from the point to the basket is nearly enough but this location must be the right of the rectangle discussed above.

Conclusions
The basketball court example shows how to map "world coordinates" to pixel coordinates to draw a basketball court familiar to many.  Many times working with "world coordinates" makes solving a graphics problem much easier than always working directly with pixel coordinates.

[A 3D version of the basketball court may be the topic of a future project when I update the existing football field project.]

Keywords:
TSimplePantograph, world coordinates, TRect, TRealRect, TPoint, TRealPoint, Point in Rectangle, arc, ellipse, Windows 95/98/2000, Delphi 3/4/5