Vector drawing with generativepy
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:
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 thedraw
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
andheight
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
andpixel_height
are the width and height of the image in pixels. These are the values that we passed intomake_image
.frame_no
andframe_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:
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:
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:
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:
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:
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:
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:
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:
First, you will notice that we have defined a couple of extra items:
- A
FillParameters
objectf
, initialised with a colour. - A
StrokeParameters
objects
, initialised with a colour, thickness, plusdash
,cap
, andjoin
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 polygoninterior_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 atp2
. - A ray has semi-infinite length. It starts at
p1
and passes throughp2
, but it extends beyondp2
right off to infinity. A ray is sometimes called a half-line. - A line has infinite length. It passes through
p1
andp2
, 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:
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:
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:
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:
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):
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:
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