Filling and outlining shapes

By Martin McBride, 2023-12-10

Now we have seen how to draw a simple shape, we will take a look at how to style shapes by filling and outlining them. This can be used to add clarity to diagrams by using colour to link related items. Colour can also make your images more interesting and appealing.

Of course, when designing graphics we should always be aware that some of our intended audience might not be able to see colour differences clearly due to colour vision deficiency (so-called colour blindness) or other visual impairments. It is always a good idea to use light and dark colours, different shapes, dashed vs solid lines, and text labelling, in addition to pure colour, to make your diagrams accessible.

Filling shapes

We have already seen how to fill a shape, but here is another example with 3 shapes:

Filling shapes

Here is the code:

from generativepy.color import Color
from generativepy.drawing import make_image, setup
from generativepy.geometry import Rectangle, Circle, Square


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), 200).fill(Color("dodgerblue"))
    Circle(ctx).of_center_radius((250, 250), 75).fill(Color("maroon"))
    Rectangle(ctx).of_corner_size((100, 300), 350, 50).fill(Color("grey", 0.7))


make_image("fill.png", draw, 500, 400)

The Square object draws the large blue square. A square is created in a similar way to a rectangle, but it only has a width (unlike a rectangle that has a width and height).

The Circle object draws the purple circle. A circle is specified in a similar way to a rectangle, except that the of_center_radius method takes a centre point and a radius value to define the circle.

The other thing to notice is that the circle overlaps the previous square. Because the square was drawn before the circle, the circle is painted over the square. Part of the square is hidden behind the circle. If we wanted to draw the square in front of the circle, we would simply need to reverse the drawing order by swapping the two lines of code.

The grey Rectangle object, lower down the image, overlaps the circle. This time the rectangle is painted over the circle. However, the rectangle colour is partly transparent, so we can still see part of the circle behind the rectangle.

We will look at other shapes, colours and transparency in more detail in a later section of the tutorial.

Shape outlines

In addition to filling shapes, we can also outline them. We do this using the stroke method rather than the fill method. Here are the same shapes as before, outlined in different colours:

Stroking shapes

Here is the code:

from generativepy.color import Color
from generativepy.drawing import make_image, setup
from generativepy.geometry import Rectangle, Circle, Square


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), 200).stroke(Color("red"), 1)
    Circle(ctx).of_center_radius((250, 250), 75).stroke(Color("blue"), 8)
    Rectangle(ctx).of_corner_size((100, 300), 350, 50).fill(Color("orange")).stroke(Color("black"), 4)


make_image("stroke.png", draw, 500, 400)

To draw a shape outline, we use the stroke method instead of the fill method. stroke requires two parameters - the colour and the stroke width. Width is measured in user space, so by default, the stroke width is specified in pixels.

The square has a stroke colour of red and a width of 1 unit (ie 1 pixel). This draws a thin line around the square.

The circle has a stroke colour of blue and a width of 8, This makes a much thicker line. Notice also that the circle is not filled, so we can see the outline of the red square behind the circle.

Filling and outlining

The final shape, the rectangle, is filled and stroked. We call fill with the colour orange, and stroke with the colour black and a width of 4.

We call fill before stroke, so that the rectangle is filled first, then outlined.

The stroke follows the outline of the shape, with the stroked area half inside the shape and half outside, like this:

Stroke boundary

The dotted white line shows the outline of the rectangle, and the 4 small red dots show the corners of the rectangle. This indicates that the stroke is half outside the boundary and half inside.

It is possible to stroke the shape first and then fill it. In that case, the fill colour will occupy the whole area of the rectangle, and the stroke will appear to be half of its defined width. This method isn't normally used.

Join styles

There are some more options for styling line strokes. It is possible to style the way the lines join, using the join parameter of the stroke method:

from generativepy.color import Color
from generativepy.drawing import make_image, setup, MITER, ROUND, BEVEL
from generativepy.geometry import Square


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).stroke(Color("black"), 30, join=MITER)
    Square(ctx).of_corner_size((200, 50), 100).stroke(Color("black"), 30, join=ROUND)
    Square(ctx).of_corner_size((350, 50), 100).stroke(Color("black"), 30, join=BEVEL)


make_image("corner.png", draw, 500, 200)

Here is the result:

Line joins

The left-hand square uses a MITER join that gives a sharp corner. The middle square uses a ROUND join that rounds the corners off. The right-hand square uses a BEVEL join that cuts the corners off with a straight line.

This is largely a matter of preference, you can use whichever you think looks best. If you have several lines or corners that all join at the same point, it is often a good idea to use the ROUND style as it can look neater.

Mitre limit

When using the mitre style, if the two sides meet at a very small angle, the joint can get very long and look quite odd. To avoid this, a mitre limit is applied. When the angle gets below a certain size the style will automatically switch to BEVEL to avoid this problem. You don't need to worry about this, it will just happen automatically and is usually a good thing.

It is possible to change this behaviour using the miter_limit parameter of the stroke method. We won't cover it in detail here because it is quite a specialised function. Refer to the official Pycairo documentation for more details.

Line caps

We can also draw straight lines, using Line objects:

Line cap

A line is defined by 2 points - the star of the line and the end of the line. When we stroke a line, we draw a line of the chosen colour and width from the start point to the endpoint.

We can choose the style of the line caps (ie the line ends) using the optional cap parameter of the stroke method:

  • SQUARE, the top line above, squares off the line ends. The two red dots indicate the line endpoints. When square caps are selected, the marked area extends slightly beyond the line's endpoints, by a distance equal to half the line width. Square is the default cap type.
  • BUTT, the middle line above, looks quite similar to the square case. The difference is that the line ends exactly on the endpoints, rather than extending beyond them.
  • ROUND, the bottom line above, creates a rounded line end. The line end is a semicircle with a radius equal to half the line width.

Line caps only apply to the ends of lines, whereas line joins apply to the corners of shapes. Shapes such as rectangles or squares don't have any ends, so the line cap parameter does not affect them. Lines have no corners, so the line join parameter does not affect them. Note that if two lines happen to meet at a point, that doesn't mean they are joined, so the appearance is controlled by the cap value rather than the join value.

Here is the code to draw the diagram above:

from generativepy.color import Color
from generativepy.drawing import make_image, setup, MITER, ROUND, BEVEL, SQUARE, BUTT
from generativepy.geometry import Square, Line, Circle


def draw(ctx, pixel_width, pixel_height, frame_no, frame_count):
    setup(ctx, pixel_width, pixel_height, background=Color(1))

    Line(ctx).of_start_end((50, 50), (350, 50)).stroke(Color("black"), 30, cap=SQUARE)
    Circle(ctx).of_center_radius((50, 50), 4).fill(Color("red"))
    Circle(ctx).of_center_radius((350, 50), 4).fill(Color("red"))

    Line(ctx).of_start_end((50, 150), (350, 150)).stroke(Color("black"), 30, cap=BUTT)
    Circle(ctx).of_center_radius((50, 150), 4).fill(Color("red"))
    Circle(ctx).of_center_radius((350, 150), 4).fill(Color("red"))

    Line(ctx).of_start_end((50, 250), (350, 250)).stroke(Color("black"), 30, cap=ROUND)
    Circle(ctx).of_center_radius((50, 250), 4).fill(Color("red"))
    Circle(ctx).of_center_radius((350, 250), 4).fill(Color("red"))


make_image("line-cap.png", draw, 400, 300)

This includes the 2 lines and the red circles that indicate the line ends.

Dash patterns

It is often useful to create dashed or dotted lines on a diagram. We can do this using the dash parameter of the stroke method. Here are some examples:

Dashed lines

The dash pattern is specified by a list of values, which specify the on and off lengths of the dashed line, in user units (pixels by default).

Looking first at the top row of shapes, they each have a dash pattern of [16]. This means that the line will be solid for 16 pixels, then a gap of 16 pixels, repeated around the whole shape. However, each dash also includes line caps, as defined above.

The square at the top left has the default line cap SQUARE. This means that, although the lines and gaps each have a nominal length of 16 pixels, the lines are extended by the square line cap. Since the line width is 8, this means that a cap of length 4 is added to each end of every line section. This means that each line has a marked length of 24 pixels. This in turn means that each gap is only 8 pixels (because the line occupies part of the gap).

The circle in the top centre uses the same measurements, but has a style of BUTT. This means that each line finishes exactly on its nominal width, so the lines and gaps have exactly equal lengths. This shape also shows how dashed lines follow curves.

The square at the top right has the same measurements but with a ROUND cap. It looks very similar to the square on the left, except that it has rounded ends rather than square ends.

The bottom row of shapes demonstrates some other effects. The square on the left uses BUTT line caps, with a short length and a longer gap, to give a light dashed line. The circle is quite interesting - the line length is 0, but because the cap style is ROUND the lines appear as circles (2 semicircular ends joined together) giving a dotted line. The square on the bottom right has a pattern of [0, 10, 3, 7]`, so it gives a dot-dash pattern.

Dash patterns can be used on any line, as we will see later. For example, you can outline text with dashed lines, and you can also apply dashes to line plots on graphs.

Here is the code for the examples above:

from generativepy.color import Color
from generativepy.drawing import make_image, setup, BUTT, ROUND
from generativepy.geometry import Rectangle, Circle, Square


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).stroke(Color("black"), 8, dash=[16])
    Circle(ctx).of_center_radius((250, 100), 60).stroke(Color("black"), 8, dash=[16], cap=BUTT)
    Square(ctx).of_corner_size((350, 50), 100).stroke(Color("black"), 8, dash=[16], cap=ROUND)
    Square(ctx).of_corner_size((50, 200), 100).stroke(Color("black"), 4, dash=[6, 12], cap=BUTT)
    Circle(ctx).of_center_radius((250, 250), 60).stroke(Color("black"), 6, dash=[0, 10], cap=ROUND)
    Square(ctx).of_corner_size((350, 200), 100).stroke(Color("black"), 4, dash=[0, 10, 3, 7], cap=ROUND)


make_image("dash.png", draw, 500, 350)

See also

Join the GraphicMaths Newletter

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

Popular tags