Shapes

By Martin McBride, 2023-12-10

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:

Simple 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:

Polygons

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:

Circles

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:

Arc angles

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:

Arc angles

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:

Popular tags