Vector drawing with generativepy

By Martin McBride, 2025-04-18

Categories: generativepy


This chapter covers the basics of creating vector images with generativepy. It provides a simple template for drawing vector images using the drawing module. It also shows how to use the geometry module to draw various shapes, and how to style those shapes.

generativepy can also be used to create bitmap images, NumPy-based images, 3D images, and videos. These will be covered in later tutorials, but the basic process is similar in all cases.

Vector images

A computer image is often stored as a 2-dimensional array of pixels, where each pixel can be any colour. This is called a bitmap image.

However, when we use vector graphics we don't usually specify the colours of individual pixels. Instead, we work at a higher level, defining shapes in the image. These can be simple shapes, such as rectangles, or more complex shapes, such as text characters.

When we use generativepy in vector mode, our program simply specifies a shape and specifies how it should be filled and outlined. The generativepy library is responsible for deciding the colour of each pixel.

The end result is still a bitmap image file, in PNG format. However, the way your code describes the image works at the level of shapes rather than pixels, which is more useful for mathematical images.

In this article, we will look at:

  • The generativepy coordinate system
  • Drawing basic shapes (lines, polygons, and circles).
  • Fill and outline styles.

There are some more advanced features of vector graphics, such as compound shapes (eg shapes with holes), complex curves, and clipping. We will look at these in a later chapter. There are also some specialised techniques for mathematical diagrams, such as creating geometric diagrams and graphs, that again we will look at in a later chapter.

Creating our first image

To start, we will create the following simple image, as a 500 by 400 pixel PNG file, consisting of an orange rectangle on a grey background:

Rectangle

Before you try this code, you will need to install generativepy and its dependencies, see the introduction

The code

Here is the code to draw the image above:

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

def draw(ctx, pixel_width, pixel_height, frame_no, frame_count):
    setup(ctx, pixel_width, pixel_height, background=Color("grey"))
    Rectangle(ctx).of_corner_size((100, 150), 250, 200).fill(Color("orange"))

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

We will now look at this code in more detail.

Basic structure

The basic structure of the code is:

  • A user-defined function draw that does the drawing.
  • A call to make_image. This function controls the process of creating an image and saving it to a file. We pass in the draw function to control the image content.

This basic structure is used throughout generativepy. We use a draw function to create the drawing, and a make_XXX function to create the image. The same pattern is used when creating bitmap images, NumPy-based images, and 3D images. The same pattern is used whether we are creating still images, animated GIFs, image sequences, or videos.

In all cases, images are stored in memory as "frames". A frame is a NumPy array containing the pixel data. This makes the library very flexible as images from different sources can be combined using simple Python and NumPy functions.

For vector images, like this one, generativepy uses the Pycairo library. As we will see, ctx is a Pycairo drawing context.

make_image

The make_image function, from the drawing module, is the main function that handles creating vector images.

The parameters are:

  • outfile, the filename of the output PNG file.
  • A draw function that does the actual drawing.
  • The width and height of the image in pixels.
  • An options channels parameter that is mainly used for creating images with transparency.

The draw function is a function that we define ourselves to perform the drawing. It doesn't have to be called draw, of course - you can call it whatever you like, provided you pass its name into make_image. The draw function must have the correct signature (in other words it must accept the parameters described below).

draw function

draw accepts 5 parameters:

  • ctx is a Pycairo context. This is like a virtual drawing surface that you can draw on in code. Whatever you draw will appear on the final image.
  • pixel_width and pixel_height are the width and height of the image in pixels. These are the values that we passed into make_image.
  • frame_no and frame_count are only used when you want to create image sequences (for anaimations), so we can ignore them here.

generativepy uses Pycairo as its drawing library. generativepy provides a rich set of high-level drawing functions for creating shapes, text, markers, graphs, and formulas that can be added to the image within the draw function

The generativepy drawing functions are built on top of the normal Pycairo functions. If you are familiar with Pycairo, it is also possible to use the Pycairo functions directly, or even to use a mixture of generativepy and Pycairo calls. Usually, though, it is better to stick with the generativepy functions as they operate at a higher level and are designed specifically for maths and diagrams.

Our draw function calls the setup function to set up the drawing area, then draws a rectangle.

setup function

The setup function isn't mandatory, but it does some useful things, so you will often want to call it.

setup has 3 required parameters - the ctx, pixel_width and pixel_height that were passed into the draw function. It has some optional parameters that do various things.

In this case, we are setting the background parameter to Color("grey") which sets the background colour to a mid-grey. We define the colour using the color module of generativepy. In this example, we are using colour names to select colours. These names are based on the CSS named colours. The color module has lots of other functionality, described in a later chapter.

Drawing a rectangle

We use a Rectangle object to draw a rectangle, like this:

Rectangle(ctx).of_corner_size((100, 150), 250, 200).fill(Color("orange"))

All shapes are drawn using the same pattern:

  • Declare a shape object, for example Rectangle(ctx). The context is passed into this function.
  • An of_xxx method is used to define the shape. In this case, of_corner_size defines a rectangle from the position of its top, left-hand corner, and its width and height. The corner is specified using an (x, y) tuple.
  • A drawing method is then used to draw the shape. In this case, we use fill which fills the shape with an orange colour.

Coordinate system

All coordinates and sizes are specified in user space coordinates.

By default, user space is measured in pixels, based on the size of the image we are creating.

  • The origin of the user space coordinate system (0, 0) is at the top left of the image.
  • x values increase from left to right.
  • y values increase as you move down the image.

In our case:

  • Our image is 500 by 400 pixels.
  • The orange rectangle is 250 by 200 pixels.
  • The position of the rectangle (that is, the position of its top-left corner) is at pixel position (100, 150).

This diagram illustrates the coordinates of the rectangle:

Coordinates

User space can be transformed, so that shapes drown in user space can be scaled, rotated, etc when they are drawn, as we will see shortly.

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

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("green", 0.4))

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 square, 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 green 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. The colour of the rectangle is set to green, but the extra parameter 0.4 is a transparency value. The rectangle is partly transparent, so we can see the red circle behind it.

You might also notice that the background colour in the setup function is Color(1). Using 1 rather than a colour name creates a grey value of 1, which is white (this is described in detail in a later chapter).

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 middle of the square is left unfilled.

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

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.

Fill and stroke position

The stroke follows the outline of the shape, with the stroked area half inside the shape and half outside. Here is an example of a filled and stroked rectangle:

Stroke boundary

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

This means that a stroked rectangle is always slightly bigger than its requested size because the stroke is centred on the exact boundary. And the filled area inside the rectangle is always slightly smaller than the requested size because the stroke covers part of the inner area. This is the typical behaviour of most computer graphics systems and is as intended. It gives the best appearance when, for example, the edges of two outlined shapes touch.

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 lines 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 MITER 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 almost always 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 two points - the start 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, ROUND, SQUARE, BUTT
from generativepy.geometry import 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 three 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 the dashes have 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)

Other shapes

generativepy supports several other shapes:

  • More polygons
    • Triangles
    • General polygons
    • Regular polygons
  • Line variants - segment, ray, or infinite line
  • Circle variants
    • Sector, segment, and arc of a circle
    • Ellipses

More polygons

This code draws several other types of polygons:

from generativepy.color import Color
from generativepy.drawing import make_image, setup, ROUND
from generativepy.geometry import Triangle, FillParameters, \
     StrokeParameters, Polygon, RegularPolygon


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

    f = FillParameters(Color("powderblue"))
    s = StrokeParameters(Color("darkslateblue"), 6, dash=[10, 8],
                         cap=ROUND, join=ROUND)

    Triangle(ctx).of_corners((50, 150), (200, 120), (100, 50)).fill(f).stroke(s)

    points = ((300, 50), (350, 100), (330, 200), (270, 200), (250, 100))
    Polygon(ctx).of_points(points).fill(f).stroke(s)

    points = ((500, 50), (550, 100), (530, 200), (470, 200), (450, 100))
    Polygon(ctx).of_points(points).open().stroke(s)

    p = RegularPolygon(ctx).of_centre_sides_radius((150, 300), 8, 70).stroke(s)
    print(p.inner_radius)

make_image("more-polygons.png", draw, 600, 400)

Here is the result:

More polygons

First, you will notice that we have defined a couple of extra items:

  • A FillParameters object f, initialised with a colour.
  • A StrokeParameters object s, initialised with a colour, thickness, plus dash, cap, and join values.

We can use f and s as parameters for the fill and stoke methods of any object. This is useful if we want to draw several objects in the same style because it saves repetition.

The top left shape is a Triangle. This has an of_corners method to define its three corners, as (x, y) tuples. It is then filled and stroked in the usual way, but using f and s to control the style.

The top centre shape is a Polygon. This has an of_points method to define its vertices. The vertices are provided as a sequence of (x, y) tuples.

The top right shape is another Polygon. This time it has an extra call to its open method, which creates an open polygon, which means that the final vertex is not connected to the first vertex. This is sometimes called a polyline because it can be thought of as a set of connected lines rather than a shape. We haven't filled this shape, but you can if you wish, that would fill the shape as if it were a closed polygon, but of course, the outline would still be open.

The bottom left is a RegularPolygon. We call of_centre_sides_radius passing in:

  • The position of the centre of the polygon, as an (x, y) tuple.
  • The required number of sides. We passed in 8 to create an octagon.
  • The radius of the shape. This controls the size of the polygon. It is the distance from the centre to any vertex of the polygon.

By default, the polygon will be drawn such that the bottom edge is horizontal (as shown). The of_centre_sides_radius function has an optional angle parameter that can be used to rotate the shape clockwise by the supplied angle, specified in radians.

A RegularPolygon has a few useful properties, calculated from the parameters:

  • side_len - the length of a side of the polygon
  • interior_angle - the interior angle of the polygon (in radians)
  • exterior_angle - the exterior angle of the polygon (in radians)
  • inner_radius - the inner radius (distance from the centre to the midpoint of any side)
  • outer_radius - the outer radius of the polygon (this is just the radius we supplied)
  • vertices - a list of the vertices as (x, y) tuples

Line Variants

We have seen how to draw a line using the Line class. This draws a straight line between two points, which here we will call p1 and p2.

In computer graphics, a line normally means a finite line between two points. But in mathematics, we define three different types of lines:

  • A line segment has finite length. It starts at p1 and ends at p2.
  • A ray has semi-infinite length. It starts at p1 and passes through p2, but it extends beyond p2 right off to infinity. A ray is sometimes called a half-line.
  • A line has infinite length. It passes through p1 and p2, but it extends to infinity in both directions.

By default, Line draws a line segment, but it can also draw rays and (infinite) lines. Here is some example code:

from generativepy.color import Color
from generativepy.drawing import make_image, setup, ROUND
from generativepy.geometry import StrokeParameters, Circle, Line

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

    def dot(p):
        Circle(ctx).of_center_radius(p, 8).fill(Color("red"))

    s = StrokeParameters(Color("darkgreen"), 6, cap=ROUND)

    p1 = (100, 300)
    p2 = (150, 100)
    Line(ctx).of_start_end(p1, p2).as_segment().stroke(s)
    dot(p1)
    dot(p2)

    p1 = (200, 300)
    p2 = (250, 100)
    Line(ctx).of_start_end(p1, p2).as_ray().stroke(s)
    dot(p1)
    dot(p2)

    p1 = (300, 300)
    p2 = (350, 100)
    Line(ctx).of_start_end(p1, p2).as_line().stroke(s)
    dot(p1)
    dot(p2)

make_image("more-lines.png", draw, 500, 400)

Here is the result:

More polygons

In the code, we have created a dot function that draws a small red circle to mark a point.

The line on the left of the image is a segment. It just extends from one point to the other. The code includes a call to as_segment, although it isn't really needed because a segment is the default. The line in the centre is a ray, that extends from the first point, through the second point, and then disappears off the edge of the image. The line on the right is an infinite line, that passes through both points and disappears off the edge of the image in both directions.

One thing to bear in mind is that the Line class doesn't actually draw an infinite line, it just draws a very long line that goes beyond the edge of the image. If you create a very large image, you might need to make the line longer. The as_line and as_ray methods accept an optional parameter infinity that you can set to a suitable large number if needed.

Circle variants

We have already seen how to draw a circle, but generativepy offers some variants:

  • A sector of a circle is like a pie slice taken out of the circle
  • A segment is part of a circle cut off by a chord (a chord is a straight line between two points on the circumference)
  • An arc is part of the circumference.

It is also possible to draw an ellipse, which is also covered in this section.

Here is the code to draw these shapes:

import math

from generativepy.color import Color
from generativepy.drawing import make_image, setup
from generativepy.geometry import Circle, FillParameters, StrokeParameters

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

    f = FillParameters(Color("yellow"))
    s = StrokeParameters(Color("black"), 6)

    (Circle(ctx).of_center_radius((150, 150), 75)
                .fill(f).stroke(s))
    (Circle(ctx).of_center_radius((350, 150), 75)
                .as_sector(math.radians(0), math.radians(135))
                .fill(f).stroke(s))
    (Circle(ctx).of_center_radius((150, 350), 75)
                .as_segment(math.radians(-90), math.radians(20))
                .fill(f).stroke(s))
    (Circle(ctx).of_center_radius((350, 350), 75)
                .as_arc(math.radians(-90), math.radians(20))
                .stroke(s))

make_image("more-circles.png", draw, 500, 500)

Here is the result:

More circles

The top left shape is a normal circle. The other shapes are described below.

Sectors

The top right shape is a sector, created with this code:

    (Circle(ctx).of_center_radius((350, 150), 75)
                .as_sector(math.radians(0), math.radians(135))
                .fill(f).stroke(s))

Here, we declare a circle as usual. But there is an extra call to as_sector. This creates a sector using the supplied angles of 0° and 135°. This requires a bit more explanation. First, we need to know that angles in generativepy are measured clockwise from the positive x-axis. This is illustrated here:

Angles

This differs from the normal mathematical convention, but that is common in computer graphics because we normally measure the y-direction downwards from the top of the image. Notice that angles above the horizontal are given negative values.

generativepy measures angles in radians. However, if you prefer to work in degrees, you can use the math.radians function to convert degrees to radians, as shown in the example code.

Finally, the sector is defined as the part of the circle created when we move clockwise from the first angle to the second angle. This is shown here:

Sector angles

The yellow shape shows how the sector is created from a circle, by taking the part of the circle between the angles 0° to 135° in the clockwise direction. If we wanted to draw the blue sector, we would need to take the part of the circle between the angles 135° to 0° in the clockwise direction. We would simply need to swap the start and end angles in the as_sector call, like this:

    (Circle(ctx).of_center_radius((350, 150), 75)
                .as_sector(math.radians(135), math.radians(0))
                .fill(f).stroke(s))

Segments

The bottom left of the original diagram shows a segment. This is formed from part of a circle by taking two points on the circumference and drawing a straight line between them (unlike a sector where we draw lines back to the centre of the circle):

Segment angles

We draw a segment by adding an as_segment call to the circle code, like this:

    (Circle(ctx).of_center_radius((150, 350), 75)
                .as_segment(math.radians(-90), math.radians(20))
                .fill(f).stroke(s))

This time the segment is formed from the part of the circle starting at -90° and moving clockwise to 20°. Again we can draw the opposite segment by swapping the angles.

Arcs

An arc is part of the circumference of a circle. It is a line, rather than a two-dimensional shape. It is shown on the bottom right of the original diagram. We create an arc like this:

    (Circle(ctx).of_center_radius((350, 350), 75)
                .as_arc(math.radians(-90), math.radians(20))
                .stroke(s))

An arc is created in exactly the same way as a segment, we just call as_arc rather than as_segment. The only difference is that an arc doesn't include the extra line joining the two points on the circumference.

Since an arc is just a line, you would not normally fill it.

Ellipses

An ellipse is like a circle that has been stretched in one direction. Here are a couple of examples:

Ellipses

We draw an ellipse using the Ellipse class, like this:

import math

from generativepy.color import Color
from generativepy.drawing import make_image, setup
from generativepy.geometry import FillParameters, StrokeParameters,\
    Ellipse, Transform


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

    f = FillParameters(Color("cyan"))
    s = StrokeParameters(Color("black"), 6)

    (Ellipse(ctx).of_center_radius((150, 150), 75, 100)
                .fill(f).stroke(s))
    (Ellipse(ctx).of_center_radius((400, 150), 100, 50)
                .fill(f).stroke(s))
    Transform(ctx).translate(300, 350).rotate(math.radians(30))
    (Ellipse(ctx).of_center_radius((0, 0), 100, 50)
                .fill(f).stroke(s))

make_image("ellipse.png", draw, 600, 500)

The Ellipse class is very similar to the Circle class, except that its of_center_radius method accepts an x-radius and a y-radius. In the top left example above, the x-radius is smaller than the y-radius, so the ellipse is taller than it is wide. In the top right example, the x-radius is larger than the y-radius, so the ellipse is wider than it is tall.

We can also create segments, sectors, and arcs of an ellipse in the same way as we do for circles.

The ellipse class only allows us to create ellipses where the two axes are aligned with the x and y directions. To create an ellipse where the axes are at an angle, we need to transform the drawing space. This is shown in the bottom, centre example, where we use Transform to rotate the drawing space by 30° before drawing the ellipse. We will cover Transform in a later chapter.

See also



Join the GraphicMaths Newletter

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

Popular tags

adder adjacency matrix alu and gate angle answers area argand diagram binary maths cartesian equation chain rule chord circle cofactor combinations complex modulus complex polygon complex power complex root cosh cosine cosine rule cpu cube decagon demorgans law derivative determinant diagonal directrix dodecagon eigenvalue eigenvector ellipse equilateral triangle euler eulers formula exercises exponent exponential exterior angle first principles flip-flop focus gabriels horn gradient graph hendecagon heptagon hexagon horizontal hyperbola hyperbolic function hyperbolic functions infinity integration by parts integration by substitution interior angle inverse hyperbolic function inverse matrix irrational irregular polygon isosceles trapezium isosceles triangle kite koch curve l system line integral locus maclaurin series major axis matrix matrix algebra mean minor axis nand gate newton raphson method nonagon nor gate normal normal distribution not gate octagon or gate parabola parallelogram parametric equation pentagon perimeter permutations polar coordinates polynomial power probability probability distribution product rule proof pythagoras proof quadrilateral questions radians radius rectangle regular polygon rhombus root sech segment set set-reset flip-flop sine sine rule sinh sloping lines solving equations solving triangles square standard curves standard deviation star polygon statistics straight line graphs surface of revolution symmetry tangent tanh transformation transformations trapezium triangle turtle graphics variance vertical volume volume of revolution xnor gate xor gate