# Arcs and Curves in Python

A curve is trickier than a line, in that it is harder to specify. The method used in Pygame is like that seen in other common graphics systems: a curve or arc is defined as a portion of an ellipse from a starting angle for a specified number of degrees, as referenced from the center of the ellipse. The angle 0 degrees is horizontal and to the right; 90 degrees is upwards (decreasing Y value). The el­lipse is defined by a bounding rectangle, specifying the upper left and lower right coordinates of a box that just holds the ellipse. In Figure 7.6, the rectangle defined by the upper left corner at (100, 50) and the lower right at (300, 200) has a center at (200, 125) and contains an ellipse slightly longer than it is high (upper left of the figure). The function that draws a curve is named arc(), and it takes the up­per left and lower right coordinates and a starting angle. The size of the arc also expressed as an angle.

In the upper right of the figure, the arc is drawn by the call

pygame.draw.arc(screen, (255,0,0), (100,50,300,200),

which means that the part of the ellipse from the 0-degree point counter clock­wise for 90 degrees will be drawn. The example at the lower left of the figure draws the curve from the 45-degree point for 90 degrees, resulting in the upper section of the ellipse being drawn. The final arc, at the lower right, uses a nega­tive angle. The call

pygame.draw.arc(screen, (255,0,0),(100,300,300,200),

starts at -60 degrees or 300 degrees. This way of specifying arcs is fine for simple examples and single curves, but makes combining many arcs into a more complex curve rather difficult. Joining the ends together smoothly is challenging.

The arc function has two variations that are important in practice. These possibilities are chord and pieslice. A chord connects the starting end points of the arc. The call

pygame.draw.arc(screen, (255,0,0),       (100,300,300,200),

has a known bounding box and center, but the actual starting and ending points of the arc are not known. Those points are needed to draw both the pieslice and chord. The equation of an ellipse centered at the point (h,k) is A better equation for the purposes here is the parametric equation, which gives the same curve. It is

x = h + a cost

y = k + b sint

for all values of t from 0 degrees to 360 degrees (0 radians to 2p radians).

In the arc call, the enclosing rectangle is (100,300,300,200), meaning that (100,300) is the upper left corner and (400,500) is the lower right. The center of the ellipse is the center of the bounding box, which is (250,400), so h=250 and k=400 in the ellipse equation.

The value of a in the equation is ½ of the width of the bounding box, and b is / of the height. In this case, a = width/2 = 150 and b = height/2 = 100. We now know the equation of this ellipse:

x = 250 + 150 cos t

y = 400 + 100 sin t

The parameter t is not the angle from the center to a point on the ellipse, though. It is an angle within a 360-degree circle that defines all points on the el­lipse. We can find the start and end points on the ellipse section and either join them with a line (chord) or draw lines from each to the ellipse center (pieslice) These functions work a bit differently from arc, in that they accept the center coordinates of the ellipse instead of the upper left. Figure 7.4 shows sample out­put from these functions, and notes a problem. The arc function was called speci­fying a thickness of 4 pixels. The result is not adequate. There are pixels missing within the lines, as if four arcs had been drawn and each was a bit different. This is a minor problem in Pygame.

There is a Pygame function that draws complete ellipses. The code

pygame.draw.ellipse(canvas, col, (x, y, w, h), t)

draws an ellipse that fits into the bounding box specified using color col and line thickness t.

### 1. Polygons

For the purposes of discussion, a polygon includes all closed regions, including ellipses and circles. In that context, the rect() function draws axis-oriented rectangular polygons as a special case. A triangle can be drawn using the polygon function:

pygame.draw.polygon(screen,(200,100,200), ((350, 350,(50,50),  (100,300)))

The vertices of the polygon are passed to the function as a tuple (or list) in the third parameter. There is no line thickness given, so the polygon is filled. Any number of vertices can be passed, meaning that we can draw any polygon we like. Regular polygons are special in that each side of a regular polygon is the same size. Specifying such as thing as a sequence of numerical coordinates can mean a certain amount of time spent with a pencil and graph paper, but it can be done in a general sense. Specify the polygon by giving the coordinates of its center. Specify the size as the distance from the center the center to any vertex, and give the number of sides desired. To draw an arbitrary polygon, split the 360-degree circle into N equal angles, where N is the number of sides. Find points at a distance R from the center of the circle at each of those angles, where R is the side specified. Now simply connect those points. Basic trigonometry results in the following code:

def regular polygon (xc, yc, r, n):

pi = 3.1415926

pi2 = pi/2

x0 = xc + r

y0 = yc

verts = []

a = 2*pi/n

for i in range(0,n):

x0 = xc + math.cos(pi2+a*i) * r

y0 = yc + math.sin(pi2+a*i) * r

verts.append([x0,y0])

pygame.draw.polygon(screen, (0,0,0), verts, 2)

Figure 7.8 shows some examples of this function in action, drawing regular polygons and a hexagonal grid. ### 2. Text

Drawing text more complicated than drawing simpler objects. We need to think about fonts. A font is saved on a file and has to be installed on the computer system. If a font is specified by a program but does not exist, then an error will occur, and either the finished image will look different from what was anticipated or an error will occur.

Drawing text is a very specialized operation and consist of three parts:

A graphics rendering class is instantiated and is assigned a font and size.

The text is drawn into a small surface.

The small surface, which is really an image containing the rendered text, is

copied to the main display surface at the correct location.

Within Pygame, the module font does the loading and rendering of fonts. Specifically, the method Font (pygame.font.SysFont) creates a new Font object from a font file on the host computer and provides the needed instance for ren­dering text. The first parameter is the name of a font as a string, like “Arial” or “Times.” If it is None, then the default font is used. The second parameter is the size of the font, in pixels. The Times font at size 14 is specified by the following code:

f = pygame.font.SysFont(“Times”,      14)

Now f can be used for rendering this specific font and size only. The object returned by pygame.font.SysFont has a method named render that will return a small image (surface) that has some specified text drawn on it based on the defined font. Rendering the text “Warning” using the variable f above is done by the following:

text = f.render(“Warning”, False, (0,0,0))

where the second argument defines whether the text is antialiased, and the third argument is the color to be used. The variable text is a Pygame surface that con­tains the image of the text. This surface is exactly the right size for the text.

Finally, this text image needs to be copied into the main display surface at the proper location. This introduces a new idea, called blitting. Blitting is basi­cally copying one image into another, a pixel by pixel copy from a source to a destination. It is accomplished using a method within the Surface named blit. In this precise situation, we want to copy the pixels in the image text into the main display surface, which has been named screen. So, screen is the destination and text is the source, and the call is as follows:

screen.blit(text, (x, y))

where (x,y) specifies where the source image text will be drawn within the des­tination. The tuple (x,y) defines the upper left coordinates in screen where the image text will be placed.

A simple function that does all of the text drawing stuff is as follows:

# Draw a text string at the given point.          **                                       def text (s, x, y, size=14, f=None):

global screen

if f == None:                               # Create a font if needed

f = pygame.font.SysFont(None, size)

text = f.render(s, 1,    (0,0,0))           # Render the

# string in black

screen.blit(text, (x, y))

This draws the string s at location (x,y) of the display Surface named screen, in black. If a font is passed, then it will be used, otherwise it will create a default font instance, and the size can be specified, or will default to 14 pixels.

### 3. Example: A Histogram

A histogram is a way to visualize numerical data. It is especially useful for discrete data like colors or political parties or choices of some kind, but can also be used for continuous data. It displays the counts of something against some other value, such as a category, a percentage of people voting for specific parties, or the heights of grade six girls. It draws bars of various heights each representing the number of entries in each category. In this example the only problem is the plotting of the histogram, but the more general programming problem would in­clude collecting and organizing the data. In this case, the program will read a data file named “histogram.txt” that contains a few key values. The program variable names and the corresponding data file values are as follows:  You should design graphical objects carefully. In this case, the histogram has the general appearance shown in Figure 7.9. This visual layout helps with the de­tails of the code, especially if the design has been drawn on graph paper, so that the coordinates can easily be determined.

Assume that the variables needed have been read from the file (see Exercise 2). Here’s what the program must do:

Create a window about 600 x 600 pixels in size.

Draw the horizontal and vertical axes (120, 80).

Draw the title and axis labels. Determine the width and height of each rectangle.

For i in range (0, ncategories)

Draw rectangle i

Draw label i

Development can now proceed according to the plan. Create a window, and set the background. Draw the title and the axes.

pygame.draw.line (screen, (0,0,0), (100,100), (100,500), 4) # Y Axis

pygame.draw.line (screen, (0,0,0), (100,500), (500,500), 4) # X axis

text(“Title goes here (large font)”, 150, 80, 24, fontt24) # Title

The horizontal axis label is in a smaller font (14 pixels) at the bottom of the canvas (y-580). It looks nicer if the text is centered. It’s challenging to do this ex­actly without actually drawing the string, so why not do that? In the text function, a Surface is created that is the right size for the string. We can use the width of this surface as the width of the string. A useful function that does this is textsize:

def textsize (s, size=14, f=None):

if f == None:                         # Create a font if needed

f = pygame.font.SysFont(None, size)

text = f.render(s, 1,   (0,0,0))      # Render the string

x = text.get size()                # What is the size

# of the Surface?

return x

The only new thing here is the method get_size(), which returns a tuple (width, height) that is the size of the Surface object. Now we can center and draw the X axis label:

hlabel = “Horizontal label here (medium font)”

cx = (400-textsize(hlabel, 14, fontt14)) / 2

text(hlabel, 100+cx, 530, 14, fontt14) # Title

Drawing the vertical label is more difficult, and so it will be done later. Make it a function:

verticalLabel(vlabel)

Now, it is time to draw the rectangles. The width of each one is the same, and it is the width of the drawing area divided by the number of categories. The height is the height of the drawing area divided by the maximum value to be drawn, maxsize. Compute those values and set the line thickness to one pixel, then set a fill color.

wid = (400-10)/ncategories     # Width of a box in pixels

ht = 390.0/maxsize             # Each value is this

# many pixels high

We then make a loop that draws each rectangle. The X position of a rectangle is its index times the width of a rectangle. The height of the rectangle is the value of that data element multiplied by the variable ht that was determined before. We also draw the value being represented at the top of the bar, which is just above and to the right of the rectangle’s upper left.

for i in range(0,ncategories):

ulx = 100 + i*wid+2                # Upper left X

uly = int(500 – val[i]*ht-0.5)     # Upper left Y (int)

pygame.draw.rect (screen, (0,0,0), (ulx, uly, wid, val[i]*ht), 1)

text (str(val[i]), ulx+15, uly-22, 14, fonth14)

The value of the histogram entry is drawn at the top of the rectangle.

Finally, draw the labels for each rectangle. These are below the X axis, cen­tered within the horizontal region for each bin. The labels start at the Y axis (X=100 or so) and their location increased by the width of the bin each iteration of the drawing loop. The Y location is fixed, at 520 – the X axis is 500. Finally, an attempt to center these labels is done in the same way that it was done for the horizontal label, but the parameters are different.

x = 100+2

for i in range (0,ncategories):

cx = wid – textsize(lab[i], 14, fonth14)

if cx < 0: cx = 0

text (lab[i], x+cx/2, 510) x = x + wid

Drawing the vertical label involves pulling out the individual words and drawing each one on its own pixel row. Words are separated by spaces (blanks), so one way of drawing the vertical text is to look for a space in the text, draw that word, then move down a few pixels, extract the next word, draw it, and so on, until all words have been drawn. The text is drawn starting at X=12, and the initial vertical position is 200, moving down (increasing Y) by 20 pixels for each word. This is done by the function verticalLabel(), which is passed the string to be drawn:

def verticalLabel(v):

lasti = 0
x = 12
y = 200
for i in range(0, len(v)): # Look at each character

if (v[i] == ” “): # Find a space

text (v[lasti:i], x, y) # Draw the text to
# the space
y = y + 20 # Increment Y
lasti = I # End of his word is start
# of next

text (v[lasti:], x, y) # Draw

This program is available on the disk (gradesHisto.py). The output is shown in Figure 7.10. This is a minimal program, and won’t always create a nice image. Labels that are too long and use too many categories can cause badly formatted graphics.

### 4. Example: A Pie Chart

A pie chart is really just a histogram where the relative size of the categories is illustrated by an angle instead of the height of a rectangle. Each class is shown as a pie-slice shape of a circle whose area is related to its proportion of the whole sample. Pie-slice-shaped regions are easy to create because we’ve already writ­ten the pieslice function. Using the same examples as before, look specifically at the grade data: there are 38 students whose grades are being displayed, and there are 360 degrees in a circle. A category of 10 students, for example (such as those receiving a “B” grade) will represent a pie slice that is 10/38 of the whole circle, or about 95 degrees. The process seems to be to determine how many degrees each category represents and draw a pie slice of that size until the whole pie (circle) is used up.

Create a window about 600 x 600 pixels in size.

Draw the title label.

Establish a fill color. For i in range (0, ncategories)

Determine the angle A used for this category i.

Draw arc from previous angle for A degrees.

Draw label i for this slice.

Change the fill color.

The labels may present a problem, as they may not fit inside the pie slice. It is probably best to display the label outside of the slice and draw a line to the slice that represents it.

The program is similar to that for the histogram. Beginning after the label is drawn, find the total number of elements in all categories (the number of students in the class). This is the sum of all elements in val.

totalSize = 0 r = 255

fill (r, 200, 200)

for i in range (0, ncategories):

totalSize = totalSize + val[i]

Each count val[i] in a category represents val[i]/totalSize of the entire data set, or the angle 360.0*val[i]/totalSize. The constant 360/totalSize is named an- glePerCount. Now starting at angle 0 degrees, and create a pie-shaped arc the size of each category:

span = val[i]*anglePerCount

pieslice(300, 300, 450, 450, angle * conv,(angle + span) * conv)

label (300, 300, 200, lab[i], 1.25,angle*conv, (angle+span)*conv)

The function label draws the text label. The angle to start drawing must be increased so that the next arc starts where this one left off:

angle = angle + span

Change the color so that each pie piece is a different color. The code below changes the red component just a little.

r = r – 20

fill (r, 200, 200)

Figure 7.7b shows a way to determine where a label could go; a line from the center of the circle through the outer edge points in the direction of the label. Simply find the x and y coordinates. The y coordinate is the sine of the angle mul­tiplied by the distance from the center, and the x coordinate is the cosine of the angle multiplied by the same distance. For a distance, use the radius multiplied by 1.5. The function label() can now be written:

def label (xx, yy, r, m, s, a1, ap):

angle = a1 + ap/2        # Bisector= start angle +

# half of span

d = r*m                  # Distance (m is usually 1.25)

x = cos (angle* d + xx   # Angle is radians

text (s, x, y, 20, fonth20)

The result is illustrated in Figure 7.11.

There are two more things that could be added to the pie chart program. Sometimes, one of the pieces is moved out of the circle to emphasize it. It turns out that this useful feature can be implemented in a manner very similar to the way the labels were drawn. Find the bisector of the angle for that section and before it is drawn, identify a new center point for that piece a few pixels down that bisector. This pulls the piece away from the original circle center. This is the function pull. Next, the pie slices should be filled with color, not just outlined. This involves some significant code, because arc does not draw thick lines well and does not fill an arc. We’ll write our own program to draw filled arcs. Start with the para­metric equations for an ellipse:

x = h + a cost

y = k + b sint

Drawing an ellipse means computing x and y for consecutive values of t between 0 and 360 degrees (2p radians) and connecting those values by lines. To fill it, use the pygame.draw.polygon method to draw the lines as a polygon, and set the line width to 0. To finish this, draw a filled triangle using the center point of the ellipse and the start and end point of the arc.

The code is brief, and is included below for your reference.  ### 5. Images

Unlike the graphical components displayed so far, an image is fundamentally a collection of pixels. A camera captures an image and stores it digitally as pix­els. Displaying an image means drawing each pixel in the appropriate color, as captured.

Pygame can load and display images in a limited fashion. Images reside in files of various formats, such as JPEG, GIF, BMP, and PNG. The same image in each format is stored in a distinct way, and it can require a lot of code just to get the pixels from the image. Pygame allows image files to be read directly: formats including GIF, PNG, JPG, and BMP are each identified by the last three charac­ters in the file name.

The function pygame.image.load will read an image file and return an image as a surface that can be displayed in the graphics window. The file “charlie.gif” is a photo of checkpoint Charlie in Berlin (Figure 7.13), and has been included on the accompanying disk. It could be read in to a Python program with the call:

The variable im now holds the image, as a Surface. We know that a Surface has a get_size method, and we can now create a display Surface and size it to be exactly as large as the image. Displaying the image involves calling the blit func­tion. The entire program to read and display this image is as follows:

import pygame

sz = im.get size()

width = sz

height = sz

screen = pygame.display.set mode((width, height))

clock = pygame.time.Clock()

pygame.init()

FPS = 10 while True:

clock.tick(FPS)

if event.type == pygame.QUIT:

quit()

screen.fill((180, 180,  180))

screen.blit(im, (0,0)) pygame.display.update()

A sample result is shown in Figure 7.13 Pixels, Again

A Surface is returned by pygame.image.load, and as such, we have read and write access to all of the pixels. We can get the value of a pixel in a Surface im using get:

pixel = im.get at((i,j))

where i and j are the x and y locations of the pixel. The returned value is a color. We can change a value in the Surface using the following code:

im.set at((i,j),c)

where c is the color to assign to the pixel at location (i,j). An image consists of rows and columns of pixels, and a pixel is a color. The color components are as follows:

red = c

green = c

blue = c

These functions operate on an image, but since the main display surface is also of the same type, they apply to it as well.

Example: Identifying a green car

There is a pattern here that is important to recognize when working with images at the pixel level – the raster scan. All of the pixels in the image are exam­ined one at a time using a nested loop. The code is as follows:

for i in range(0, width):

for j in range(0, height):

# Do something to pixel (i,j)

This example uses color to identify the pixels that belong to a car in an image, as seen in Figure 7.10. The problem requires identifying pixels that are green and making them stand out in the image. All pixels have a green component. When something is green, the green component is the most significant one; it is larger than the red and blue components by some margin. In this case, that margin is ar­bitrarily set at 20 (if it does not work, then it can be modified). If a pixel is green, it will be set to black; otherwise, it will become white; this will make the pixels that belong to the car stand out. The program begins by creating a window and reading in the image:

sz = im.get size()

width = sz

height = sz

screen = pygame.display.set mode((width, height))

Now look at all of the pixels, searching for a green one:

for i in range(0, width):

for j in range(0, height):

c = im.get at((i, j))     # Get the color of the

# pixel (i,j)

If the pixel is green, then change it to black. Otherwise, change it to white:

if c > (c + 20) and c > (c + 20):

im.set at((i, j), (0, 0, 0))

else:

im.set at((i, j),  (255, 255, 255))

Display it and the program is complete (Figure 7.14b). Note that there are some green pixels that do not belong to the car, but most of the car pixels have been identified. \

Example: Thresholding

Image processing is a large subject, and this particular book is not the best choice for exploring it in detail. There are some basic actions that can be done, and common ones include thresholding, edge enhancement, noise reduction, and count, all of which can be done using in Python and drawn using Pygame. Thresholding in particular is an early step in many image-analysis processes. It is the creation of a bi-level image, having just black and white pixels, from a grey or color image. The previous example is different from thresholding in that a particular color was being searched for. In thresholding a simple grey value T, the threshold, is used to separate pixels into black and white: all pixels having a value smaller than T will be black, and the others will be white.

We can convert an RGB value to a single grey level by simply averaging the three color components: (red+green+blue)/3. This could be coded as function grey(), which converts a color into a simple grey level, which is an integer in the range 0 to 25. The thresholding program begins in the same way as did the pre­vious example. Look at the color of all of the pixels in the image, one at a time:

startdraw(640, 480)

for i in range(0, Width()):

for j in range(0, Height()):

c = getpixel(im, i,j) This is the standard scan of all pixels. Now convert the color c to a grey level and compare that against the threshold T=128. Pixels with a grey level below 128 are set to black, the remainder are white:

for i in range(0, width):

for j in range(0, height):

c = im.get at((i, j))

g = (c+c+c)/3 if g < T:

im.set at((i, j),  (0, 0, 0))

else:

im.set at((i, j),  (255, 255, 255))

The result, the image displayed by this program, is shown in Figure 7.15.

Transparency

A GIF image can have one color chosen to be transparent, meaning that it will not show up and any pixel drawn previously at the same location will be visible. This is very handy in games and animations. Images are rectangular, whereas most objects are not. Consider a small image of a doughnut; the pixels surrounding the donut and in the hole can have the pixels set to be transparent. Then, when the image is drawn, the background will be seen through the hole.

The transparency value must be set within the image by a program. Photo­shop, for example, can do this. Then, when Python displays the images, the back­ground image must be displayed first, followed by the images with transparency. As an example, Figure 7.16a shows a photo of the view through the rear and side
windows of a Volvo. The window glass area, the places where transparency is desired, is colored yellow. The color yellow was then selected in Photoshop to be transparent, and the image was saved again as a GIF. The short Python program given here can display a background image and the car image over the top of the background, and the background will be seen through the window regions, as shown in Figure 7.16b. im = pygame.image.load(“../07images/car.gif”)      # car image

# Background image

sz = im.get size() width = sz height = sz

screen = pygame.display.set mode((width, height))

clock = pygame.time.Clock()

pygame.init()

FPS = 10

while True:

clock.tick(FPS)

for event in pygame.event.get():

if event.type == pygame.QUIT:

quit()

screen.blit(s, (0,  0))

screen.blit(im, (0,  0)) pygame.display.update()

### 6. Generative Art

In generative art, artwork is generated by a computer program that uses an algorithm created by the artist. The artist is the creative force, the designer of the visual display, and the computer implements it. There are many generative artists to be found on the Internet: one list can be found online: http://blog.hvidtfeldts. net/index.php/generative-art-links/

Much generative art is dynamic: it involves motion and/or interaction, but many works are equivalent to paintings and drawings (static). Pygame could be a tool for helping to render these sorts of generative art works. Unlike other sorts of computer programs, those associated with art do not have a known predictable result that can be affirmed as correct. It is true that an artist begins with an idea of what their work should look like and what the message underlying it is, but paint­ings, sculptures, and generative works rarely finish the way they began.

Either begin with an idea of what the image will look like or describe the idea using a sentence or two. Here’s an example sentence: “Imagine a collection of straight lines radiating from a set of randomly placed points within the drawing window, with each set of lines drawn in a saturated strong color.”

Now an attempt would be made to create such an image using the functions that Pygame offers. It is often the case that the first few tries are in error, but that one of them is interesting. An artist would pursue the interesting result instead of sticking to the original idea, of course. Here is an example: the code below was written with the idea that it would produce a collection of lines radiating from the point (400,600) from 0 degrees (horizontal right) to 180 degrees (horizontal left) with the color varying slightly:

r = 255

for i in range(1, 180, 2):

pygame.draw.line(screen, (128, r, 128),   (x, y),

The Y coordinates should have been inverted. Instead, this created a much more interesting image. Sometimes a small error can result in a more interesting result. This is rarely the case when writing scientific or commercial software. The code for one of the other loops in the final code is as follows:

x = randrange(100, 800)

y = randrange(100, 500)

r = 255

for i in range(1, 180, 2):

pygame.draw.line(screen, (r, 200, r), (x, y),

pygame.draw.line(screen, (r, 200, r), (x, y),

r = r – 0.5

This draws some more lines in a nest set of colors from a new location. Generative art should be under the control of the artist, but it does use ran­dom elements to add interest to the image. In the piece Snow Boxes by Noah Larsen, a set of rectangles is drawn, but the specific size and location of these rectangles is random within constrained parameters. The overall color is also ran­dom within specified boundaries. Each rectangle is drawn as a collection of white pixels with a density that has been defined specifically for that rectangle so that the image consists of spatters of white pixels that can be identified as rectangular regions (Figure 7.17). Each time the program is executed, a different image is created. The program for Snow Boxes was originally written in a language called Processing, but a Python version that uses Pygame is:

# Snow boxes

# Original by Noah Larsen, @earlatron

screen.fill ( (randrange(0, 75), randrange(150, 255), randrange(0, 75))     )

fill = (255,255,255) for i in range(0, 10000):

screen.set at((randrange(0, width), randrange(0, height)), _ fill)

for i in range(0, 20):

xs = randrange(0, width) ys = randrange(0, height)

xe = randrange(xs, xs + randrange(30, 300)) ye = randrange(ys, ys + randrange(30, 300))

for j in range(0, 10000):

screen.set at( (randrange(xs,xe + 1), randrange(ys,ye + 1))    , fill) Source: Parker James R. (2021), Python: An Introduction to Programming, Mercury Learning and Information; Second edition.