Tutorial: The Point, Line, and Rectangle classes

Table of Contents


Use the Point, Line, and Rectangle classes to simplify your geometry calculations.

Level: Beginner

Platforms: Windows, macOS, Linux, iOS, Android

Classes: Point, Line, Rectangle, Path, Random, Range

Getting started

Download the demo project for this tutorial here: PIP | ZIP. Unzip the project and open it in your IDE.

If you need help with this step, see Tutorial: Projucer Part 1: Getting started with the Projucer.

The demo project

The demo project performs some simple drawing operations. In its default state it draws a grey background with a small orange square in the top-left corner:

tutorial_point_line_rectangle_screenshot1.png
Simple drawing of a square

We are going to look at different ways of drawing lines and rectangles, and how the Point, Line, and Rectangle classes can simplify the way you think about drawing operations (and component positions) in JUCE.

Rectangle basics

A great deal of graphics drawing and component layout code needs to deal with rectangles. This tutorial starts with the simple drawing of a filled square. We are now going to explore how the Point, Line, and Rectangle classes can help us with drawing operations. We can also apply these techniques to the positioning of child components (in your Component::resized() function). Our starter code in the paint() function is this:

void paint (Graphics& g) override
{
g.fillRect (10, 10, 40, 40);
}

Here we specify the bounds of the rectangle directly to the Graphics::fillRect() function as separate integers:

g.fillRect (10, // x
10, // y
40, // width
40); // height

While this is straightforward, if we specify the bounds as a Rectangle object, it becomes much easier to perform manipulations of the rectangle.

Using the Rectangle and Point classes

It is easy to replace these separate coordinates, width, and height values with a Rectangle object, like so [1]:

void paint (Graphics& g) override
{
Rectangle<int> area (10, 10, 40, 40); // [1]
g.fillRect (area);
}

While this is a very similar way of drawing the square, we can now very easily move the square around the component bounds, keeping its original size. The Rectangle class is a template class, as you can see above. In this example we use an int template parameter. In JUCE drawing code we will commonly use either Rectangle<int> or Rectangle<float> objects. The Point and Line classes are also template classes, and again we commonly use either int or float template parameters.

There are other ways to create rectangles. For example, rather than specifying width and height, we might have two points that we want to use as corners of the rectangle. We can use the Point class and a different Rectangle constructor:

void paint (Graphics& g) override
{
Rectangle<int> area (Point<int> (10, 10),
Point<int> (50, 50));
g.fillRect (area);
}

One of the great things about this technique is that we can specify any two points. These points don't need to represent the top-left and bottom-right corners of the rectangle. Therefore, an equivalent would be:

Rectangle<int> area (Point<int> (10, 50),
Point<int> (50, 10));

Using the Point and Path classes

In fact, using a Path object we can specify a rectangle with four points defining each corner:

void paint (Graphics& g) override
{
Path path;
path.startNewSubPath (Point<float> (10, 10));
path.lineTo (Point<float> (50, 10));
path.lineTo (Point<float> (50, 50));
path.lineTo (Point<float> (10, 50));
path.closeSubPath();
g.fillPath (path);
}
Note
In this case we need to use the Point<float> class, since that is what the Path class requires us to use. Although we can use the Point<int>::toFloat() and Point<float>::toInt() functions to convert between the two types of point.
Exercise
Try changing the points in the path to create other quadrilateral shapes.

We can also add a rectangle directly to a path:

void paint (Graphics& g) override
{
Path path;
Rectangle<float> area (10, 10, 40, 40);
path.addRectangle (area);
g.fillPath (path);
}
Note
We could have used a Rectangle<int> object in this case, as the Path class will convert this to the floating-point version on our behalf.

Testing points within rectangles

Another useful feature of a Rectangle object is that it can test whether it contains a specified point. To test this out, we are going to get our component to repaint itself each time we click the mouse. To do this, add the following function:

void mouseDown (const MouseEvent&) override
{
}

Now let's write our paint() function such that it randomly generates a rectangle and a point. Then it will draw the rectangle and then a smaller rectangle located around the point. This smaller rectangle will be drawn in a different colour depending upon whether the randomly-generated point is within the larger rectangle or not. Our paint() function should start as before, but we're going to generate some random values so let's cache a reference of the system Random object too.

void paint (Graphics& g) override
{
auto& random = Random::getSystemRandom();
//..

Now we'll create our random rectangle. Add the following code:

//..
Range<int> rectRange (20, getWidth() / 2);
Rectangle<int> rectArea (random.nextInt (rectRange),
random.nextInt (rectRange),
random.nextInt (rectRange),
random.nextInt (rectRange));
g.drawRect (rectArea, 2.0f);
//..

We limit the range of random values for each of the rectangle elements using a Range object. We also draw (rather than fill) the rectangle with a thickness of 2 points. To draw the smaller rectangle we are going to need another Rectangle object. If we use only two arguments to the Rectangle constructor, it creates a Rectangle object with the specified width and height (in that order) at a position of zero (0, 0). Add this line:

//..
Rectangle<int> pointArea (10, 10);
//..

Now let's randomly-generate a point and position the centre of the pointArea rectangle at that point. Add the following code:

//..
Point<int> point (random.nextInt (Range<int> (0, getWidth())),
random.nextInt (Range<int> (0, getHeight())));
pointArea.setCentre (point);
//..

This demonstrates another useful feature of the Rectangle class as we can position its centre if we wish. This is sometimes preferable to considering a rectangle's position to be its top-left corner. Now we can use the Rectangle::contains() function to determine whether the point object is within the bounds of the rectArea object. Add this code:

//..
g.setColour (rectArea.contains (point) ? Colours::limegreen
g.fillRect (pointArea);
}

Run the application, it should look something like the following screenshot. Remember to click on the component in order to cause it to redraw:

tutorial_point_line_rectangle_screenshot4.png
Testing points within rectangles
Note
The code for this example can be found in the PointLineRectangleTutorial_02.h file of the demo project.

Dealing with lines

Drawing and dealing with lines is similarly straightforward. The following code draws a diagonal line:

void paint (Graphics& g) override
{
Line<float> line (Point<float> (10, 10),
Point<float> (50, 50));
g.drawLine (line, 2.0f);
}

Line intersections

The Line class can also perform line intersection tests. To test this out we are going to generate several randomly-generated lines, then we are going to draw a circle at the points where any of these lines intersect with any of the other lines. First let's set up our paint() function with a background and get ready for generating some random numbers:

void paint (Graphics& g) override
{
auto& random = Random::getSystemRandom();
//..

Now let's generate the random lines, not only drawing them, but also storing them in an array. Add the following code:

//..
Range<int> lineRange (0, getWidth());
auto numLines = 10;
for (auto i = 0; i < numLines; ++i)
{
Line<float> line (random.nextInt (lineRange),
random.nextInt (lineRange),
random.nextInt (lineRange),
random.nextInt (lineRange));
lines.add (line);
g.drawLine (line, 2.0f);
}
//..

Then we'll change our colour and prepare a square which we will use as the bounds within which to paint our circle. Add this:

//..
Rectangle<float> pointArea (8, 8);
//..

Finally, we'll iterate over the array of lines checking for intersections with any of the other lines using the Line::intersects() function. We then move the centre of the pointArea rectangle to this point and paint a circle. To do this, add the following code:

//..
for (auto lineI : lines)
{
for (auto lineJ : lines)
{
if (lines.indexOf (lineI) != lines.indexOf (lineJ))
{
Point<float> intersection;
if (lineI.intersects (lineJ, intersection)) // [2]
{
pointArea.setCentre (intersection);
g.fillEllipse (pointArea);
}
}
}
}
}

The piece of code that checks for the intersection [2], calls the Line::intersects() function. This not only returns true if the lines actually intersect, but also returns the point at which they intersect in the intersection argument.

Run the application now. If you left in the code to repaint in response to mouse clicks then you can generate new sets of lines by clicking on the component.

tutorial_point_line_rectangle_screenshot5.png
Line intersections
Note
The code for this example can be found in the PointLineRectangleTutorial_03.h file of the demo project.

If we didn't check the bool returned by the Line::intersects() function, or we used the Line::getIntersection() instead, then points would be drawn in places where the lines would intersect should they be extended in each direction to an infinite length. For example, look at the following code:

//..
if (lines.indexOf (lineI) != lines.indexOf (lineJ))
{
Point<float> intersection;
pointArea.setCentre (lineI.getIntersection (lineJ));
g.fillEllipse (pointArea);
}
//..

This would generate something like the following screenshot:

tutorial_point_line_rectangle_screenshot6.png
Line intersections beyond the endpoints
Exercise
The final for() loop in the code for this example is simple, but not ideal. The problem is that it checks each line against every other line twice. Rewrite this code such that it only checks each pair of lines once. You should be able to remove the if (lines.indexOf (lineI) != lines.indexOf (lineJ)) statement as part of this rewrite.

Manipulating rectangles

Now let's look at some more manipulations of rectangles that we can perform. Before we start, let's add a simple function to generate a random colour (see Tutorial: The Random class) as we're going to be doing this quite a number of times in the following examples:

static Colour getRandomColour()
{
auto& random = Random::getSystemRandom();
return Colour (random.nextInt (256),
random.nextInt (256),
random.nextInt (256));
}

Now, let's extend the code for drawing the rectangle by using the Rectangle class, by drawing ten squares in a diagonal pattern, and using a randomly-generated colour:

void paint (Graphics& g) override
{
Rectangle<int> area (10, 10, 40, 40);
auto numSquares = 10;
for (auto i = 0; i < numSquares; ++i)
{
g.setColour (getRandomColour());
g.fillRect (area);
area.translate (30, 30); // [3]
}
}

The Rectangle::translate() function [3] moves the given rectangle by the horizontal and vertical offsets provided. The result should look something like the following screenshot:

tutorial_point_line_rectangle_screenshot2.png
Diagonal pattern of squares

Here is an extension to that code that resizes the rectangle before the next drawing operation. In addition to this, the translation is performed such that it is equal to the width and height of the rectangle.

void paint (Graphics& g) override
{
Rectangle<int> area (10, 10, 40, 40);
auto& random = Random::getSystemRandom();
auto numSquares = 10;
for (auto i = 0; i < numSquares; ++i)
{
g.setColour (getRandomColour());
g.fillRect (area);
area.translate (area.getWidth(), area.getHeight()); // [4]
area.setSize (random.nextInt (Range<int> (20, 40)), // width
random.nextInt (Range<int> (20, 40))); // height
}
}

This causes the rectangles to be "joined" at their corners as shown in the following screenshot:

tutorial_point_line_rectangle_screenshot3.png
Rectangles joined at the corners

As an alternative to using the Rectangle::translate() function, we could instead use addition to "add" points to rectangle in order to translate them. This means we could replace [4] above with the following line:

area += Point<int> (area.getWidth(), area.getHeight());

Rectangle intersections

If we have rectangles that overlap, we can determine the intersecting area using the Rectangle::getIntersection() function. In the following example we calculate the intersecting rectangle between the current and next rectangles in the series. To make this clear, we draw outlines for each rectangle in the series and highlight the intersecting areas by drawing them as filled rectangles.

void paint (Graphics& g) override
{
Rectangle<int> area (10, 10, 40, 40);
auto& random = Random::getSystemRandom();
Range<int> rectRandomRange (20, 40);
auto numSquares = 10;
for (auto i = 0; i < numSquares; ++i)
{
// [5]
auto nextArea = area + Point<int> (random.nextInt (rectRandomRange),
random.nextInt (rectRandomRange));
g.setColour (getRandomColour());
g.fillRect (area.getIntersection (nextArea)); // [6]
g.setColour (getRandomColour());
g.drawRect (area, 2.0f); // [7]
area = nextArea;
}
}

Notice that we use the + operator to offset the rectangle [5]. We also fill the intersecting areas first [6] before drawing the rectangle outlines [7]. This causes the last intersecting area to be drawn without its final corresponding rectangle.

tutorial_point_line_rectangle_screenshot7.png
Rectangle intersections
Note
The code for this final example can be found in the PointLineRectangleTutorial_04.h file of the demo project.

Summary

In this tutorial we have introduced the template classes Point, Line, and Rectangle. In particular, we have covered the following techniques:

See also