# Shapes

generativepy provides a variety of shape classes, that provide a consistent way to fill and stroke different shapes.

Every shape class is a subclass of `Shape`

. All shapes are defined in the `geometry`

module.

All shapes follows the same pattern:

- A constructor creates the class, accepting a context object that represents the image it will draw on.
- An
`of_XXX`

method that sets the basic shape parameters. This varies depending on the shape. - Possibly some optional modifiers that enable variants of the basic shape.
`fill`

and`stroke`

methods to fill or outline the shape (or both).

These methods are designed to be used in the *fluent programming* paradigm, where several methods are chained together on a single line. However, that is entirely optional, so you are not obliged to use that style if you don't wish to.

We will start by covering all the basic simple shapes, before moving on to a few special cases - polygons, circles, ellipses, and regular polygons.

## Simple shapes

generativepy provides `Rectangle`

, `Square`

, `Triangle`

, and `Line`

shapes. These can also be implemented using the general `Polygon`

shape, but the specialised shapes are often clearer and easier to use. Here are examples of the shapes:

These shapes have been drawn on a background that shows a pixel grid, for illustration. This grid is drawn by some additional generativepy code, shown below. It won't normally appear on a generativepy image, of course.

Here is the code to draw the image above:

```
from generativepy.color import Color
from generativepy.drawing import make_image, setup
from generativepy.geometry import Rectangle, Square, Triangle, Line
def draw(ctx, pixel_width, pixel_height, frame_no, frame_count):
setup(ctx, pixel_width, pixel_height, background=Color(1))
Square(ctx).of_corner_size((50, 50), 100).fill(Color("yellow")).stroke(Color("black"), 12)
Rectangle(ctx).of_corner_size((200, 50), 150, 75).fill(Color("tomato")).stroke(Color("olive"), 8)
Triangle(ctx).of_corners((400, 150), (550, 120), (450, 50)).fill(Color("powderblue")).stroke(Color("darkslateblue"), 4)
Line(ctx).of_start_end((150, 200), (350, 150)).stroke(Color("darkslategrey"), 8)
make_image("simple-shapes.png", draw, 600, 200)
```

The `Square`

object uses `of_corner_size`

to set its position and size. This takes 2 parameters. The first is a tuple that gives the position of its top left corner. This is set to `(50, 50)`

, and as you can see from the grid in the image, the corner of the square is indeed at (50, 50). Remember, of course, that the boundary of a shape is centred on the shape's true outline - that is, half of the boundary is inside the shape, half is outside.

The `Rectangle`

object also uses the `of_corner_size`

method, but this time it has 3 parameters. The first tuple, again, is the position of the corner, this time at `(200, 50)`

. The next 2 parameters are the width and height of the rectangle, which in this case are 150 and 75.

The `Triangle`

shape works in a different way. It has an `of_corners`

method that accepts 3 tuples that represent the corners of the triangle. This gives a lot of flexibility in defining the triangle.

We have already seen the `Line`

class in the earlier section on line ends, it is just included here for completeness.

## Polygons

The `Polygon`

class can be used to create any type of general polygon. Here are some examples:

The `Polygon`

class has a method `of_points`

that accepts a list if tuples, one for each vertex of the polygon. The polygon is drawn by connecting the vertices with straight lines.

It also has an optional `open`

method. If that method is called, the polygon is left open. This means that the final point is not connected back the first point (like the two polygons at the bottom of the image above).

Here is the code for the image:

```
from generativepy.color import Color
from generativepy.drawing import make_image, setup, ROUND
from generativepy.geometry import Polygon
def draw(ctx, pixel_width, pixel_height, frame_no, frame_count):
setup(ctx, pixel_width, pixel_height, background=Color(1))
(
Polygon(ctx)
.of_points(((100, 50), (150, 100), (130, 200), (70, 200), (50, 100)))
.fill(Color("firebrick"))
.stroke(Color("seagreen"), 6)
)
(
Polygon(ctx)
.of_points(
((200, 50), (300, 200), (400, 50), (400, 150), (300, 100), (200, 200))
)
.fill(Color("yellow"))
.stroke(Color("goldenrod"), 4)
)
(
Polygon(ctx)
.of_points(((50, 400), (100, 300), (150, 250), (250, 300), (250, 400)))
.open()
.stroke(Color("darkred"), 8, cap=ROUND)
)
(
Polygon(ctx)
.of_points(((300, 250), (400, 250), (400, 350), (350, 400)))
.open(True)
.fill(Color("lightblue"))
.stroke(Color("darkcyan"), 6)
)
make_image("polygons.png", draw, 450, 450)
```

The red pentagon in the top left is drawn using 5 points. It is then filled in red and stroked in green. The yellow shape in the top right shows hwo we can draw an intersecting polygon. The vertices of the polygon are always drawn in the order that they appear in the list.

The shape in the bottom left shows an open polygon because it calls the `open`

method. The last point at (350, 400) is not joined to the first point at (300, 250). Notice that the 2 open ends of the shape are rounded, as specified by the `ROUND`

line cap specifier in the `stroke`

method.

The shape in the bottom right is another open polygon. This time we have also filled the polygon. The fill happens as if the polygon were a close polygon, but the stroke still leaves the final line undrawn. This is a slightly unusual effect that you might not need to use very often.

Notice that, in the final shape, we call `open`

with a parameter `True`

. This parameter is optional. If we call `open`

and pass in `False`

, then the polygon will be closed (as if `open`

had never been called). This can be useful if you ever need to make a polygon optionally open of closed based on a condition.

## Circles

We looked at the `Circle`

class earlier, but in this section we will look at extra modifiers that can be used to create sectors, segments, and arcs. Here is an image illustrating these features:

Here is the code:

```
import math
from generativepy.color import Color
from generativepy.drawing import make_image, setup
from generativepy.geometry import Circle, Text
def draw(ctx, pixel_width, pixel_height, frame_no, frame_count):
setup(ctx, pixel_width, pixel_height, background=Color(1))
(
Circle(ctx)
.of_center_radius((150, 150), 75)
.fill(Color("yellow"))
.stroke(Color("black"), 6)
)
Text(ctx).of("A", (75, 75)).size(30).fill(Color("black"))
(
Circle(ctx)
.of_center_radius((300, 150), 75)
.as_sector(math.radians(0), math.radians(135))
.fill(Color("yellow"))
.stroke(Color("black"), 6)
)
Text(ctx).of("B", (325, 125)).size(30).fill(Color("black"))
(
Circle(ctx)
.of_center_radius((150, 350), 75)
.as_segment(math.radians(-90), math.radians(20))
.fill(Color("yellow"))
.stroke(Color("black"), 6)
)
Text(ctx).of("C", (125, 325)).size(30).fill(Color("black"))
(
Circle(ctx)
.of_center_radius((300, 350), 75)
.as_arc(math.radians(-90), math.radians(20))
.stroke(Color("black"), 6)
)
Text(ctx).of("D", (300, 325)).size(30).fill(Color("black"))
make_image("circles.png", draw, 450, 450)
```

The first circle, labelled **A**, is just a normal circle. The `of_center_radius`

method specifies a centre of `(150, 150)`

and a radius of 75.

**B** shows a *sector* - that is, a pie slice. This is created by adding an extra call to the `as_sector`

method. This method takes two angles that control which part of the circle is included in the sector, as explained below.

**C** shows a *segment*. A segment is created when a chord of the circle divides the circle into 2 parts. This is created by calling the `as_segment`

method, again with 2 angles.

**D** shows an *arc*. An arc is part of the circumference of the circle. This is created by calling the `as_arc`

method, again passing in 2 angles. Notice that we haven't called `fill`

in this case, because an arc is a line rather than a shape. You can fill an arc - it will fill the same area as the segment, but it will not draw a line between the 2 ends of the arc. This is not normally what you would want to do, it is usually better to draw a segment rather than a filled arc.

## Arc angles

The `as_sector`

, `as_segment`

, and `as_arc`

methods each take a start and stop angle. The angles are defined clockwise from the positive x-axis like this:

The horizontal line from the centre towards the right is defined to be 0. If we move in a clockwise direction, the angle is positive and increasing. If we move counterclockwise, the angle is negative and decreasing.

A sector is defined by its stat and end angle. The sector starts at the start angle and moves clockwise to the end angle. Here, the sector on the left starts at -45° and ends at 90°, in a clockwise direction:

The sector on the right starts at 90° and ends at -45°, again in a clockwise direction. In that case, the sector covers an angle of more than 180°, making it a *major sector*.

Arcs and segments are specified in the same way.

There are a couple of things to note about this. First, we have described this using angles measured in degrees, but in fact generativepy uses radians to measure angles. If you wish to work in degrees, you can use the `radians`

function from the Python standard `math`

library to convert degrees to radians. This is shown in the example code above.

Also, when specifying angles, adding or subtracting any multiple of 360° will not affect the result. So, for example, the angle -45° can be expressed as 315° if you prefer. If you are working in radians, adding or subtracting any multiple of 2π will not affect the result.

## Ellipses

An ellipse can be thought of as a kind of stretched circle.

## See also

## Join the GraphicMaths Newletter

Sign up using this form to receive an email when new content is added: