Filling and outlining shapes
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:
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:
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:
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:
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:
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:
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: