This article explains how to create some simple mathematical shapes with graphics.py, a popular graphics library for Python written by John Zelle. Graphics.py is a single file containing graphics functions such as Point, Line, Circle and Rectangle. In this article though, we are just going to use it to plot single points.
At the top of the page is a blancmange like shape. The program that drew it is at the bottom of the article, if you want to jump straight there. Otherwise, a couple of simpler plots will be demonstrated first, just to show a couple of underlying principles.
Installing graphics.py can be as simple as downloading the latest version of the file. You can just leave it in the same directory as your python script.
$ wget https://mcsp.wartburg.edu/zelle/python/graphics.py
Plot a Circle
With graphics.py installed, run the following Python script. It will draw a circle by plotting sine against cosine:
#!/usr/bin/python from __future__ import division from graphics import * import math height = 800 width = 1200 radius = 200 win = GraphWin(sys.argv, width, height, autoflush=False) win.setCoords(-width/2, -height/2, width/2, height/2) for a in range(0,360): ar = math.radians(a) pt = Point(radius*math.sin(ar), radius*math.cos(ar)) # Circle #pt = Point(2*radius*math.sin(ar), radius*math.cos(ar)) # Elipse #pt = Point(radius*math.sin(2*ar), radius*math.cos(3*ar)) # Lissajous pt.draw(win) win.update() print "Done" win.getMouse() # Pause to view result win.close()
Run the program and a window should appear, looking like the image below. When you are ready, click the window to dismiss it and end the program.
Plot an Ellipse
Edit the above script. Change the “
pt = ” line to add a multiplying factor of “2*” to the x coordinate. Or, just uncomment the “Ellipse” line and comment out the “Circle” line.
Run the program and it now draws am ellipse. Basically just a squashed circle, twice as wide as it is high:
If you introduce a multiplying factor to the angle (a) instead, a different effect is seen. Edit the program. Comment out the “Elipse” line and uncomment the “Lissajous” line. This is like the circle again, except that the x angle is multiplied by 2 and the y angle by 3.
Run the program and a swoopy pattern appears. It is still a plot of sine against cosine, but the “frequencies”, or wavelengths, of x and y are out by a factor of 1.5. You can vary those numbers to produce more complicated patterns.
At the top of this article is a figure shaped a bit like a blancmange, or a sort of Mexican hat. The program to draw it is below. It can also be downloaded directly from Github, or with wget, like so:
$ wget https://raw.githubusercontent.com/webtaster/Blancmange/master/blanc.py
More accurately, the shape could be called a radial sinusoid, decaying as 1/x from the origin. Although it might seem more complicated than the plots above, this “3D” surface plot is built using similar principles. Plot sine against cosine and you get a circle (a trigonometrical identity). Plot sin or cos alone and you get a wave. Pythagoras can be used to work out radial distances.
#!/usr/bin/python from __future__ import division from graphics import * import math # Dimensions of graphics window height = 800 width = 1200 # Radius of shape about the Y axis radius = 400 # Viewing angle, in degrees to the horizontal va = 10 # Wavelength of sine wave in pixels wavelength = 170 # Height of the figure at the centre (see exponential decay, below) centre_amplitude = 500 # Plot granularity. Affects texture but not overall shape xstep = 1 zstep = 10 # Viewing angle doesn't change, so calculate its sine and cosine now (optimization). var = math.radians(va) sinvar = math.sin(var) cosvar = math.cos(var) win = GraphWin(sys.argv, width, height, autoflush=False) win.setCoords(-width/2, -height/2, width/2, height/2) # Image is symmetrical, so iterate x over half the full range (-radius to radius) # and plot both halves at the same time (optimisation). for x in range(-radius, 0, xstep): # Pre-set maximums for hidden surface removal later maxy = -height miny = height # Optimisation: calculate and store this once instead of for every z later xsquared = x*x # The start (and end) of the z range for this x. Pythagoras. zlimit = int(math.sqrt(radius*radius - xsquared)) # This range is really just -zlimit to zlimit, step zstep. The reverse # and addition here is just so that the range is symmetrical about zero, # making the pixels line up properly even at high viewing angles. # A simply range(-zlimit, zlimit) produces the same shape but the # texture is not uniform. for z in list(reversed(range(0, -zlimit, -zstep))) + range(0, zlimit, zstep): # Distance of this point from the Y axis, horizontally. r = math.sqrt(xsquared + z*z) # Amplitude of wave # Reciprocal decay: more pointy amplitude = 5000*(1/(0.5*r+1)) # Exponential decay: smoother #amplitude = centre_amplitude*(0.990**r) # y is a sinusoid of r, giving the wavy shape. y = amplitude*math.sin(math.radians(r*(360/wavelength))) # "Rotate" the 3D image towards the viewer, about the x axis, to give # the viewing angle projection projy = z*sinvar + y*cosvar # Plot the points if (projy > maxy) or (projy < miny): pt = Point(x, projy) pt.draw(win) pt = Point(-x, projy) pt.draw(win) # Hidden surface removal if projy > maxy: maxy = projy if projy < miny: miny = projy # Tp speed up the drawing, remove the indent of the following line # To slow down, increase the indent, putting it into the z loop win.update() # Wait for user to click on image or terminate program print "Done" win.getMouse() win.close()
The shape takes about half a second to draw on my i7 laptop, and about 8 seconds on a raspberry pi 2. For comparison, I plotted the same curve using BASIC on an Amstrad CPC464 in the late 1980’s, where it finished in about 5 minutes (and that was with a radius of about 200, not 400). On a Dragon 32 (8 bit, 0.6 Mhz) took about 10 minutes or more.
Change the Parameters
Altering some parameters near the top of the script changes the produced image. For example, try changing the viewing angle, va (currently 10 degrees) to different values between 0 and 90. It represents the angle above the horizontal from which the user sees the image. The effect of radius is fairly obvious. Reducing the wavelength will give you more ripples.
Changing the “decay” rate alters the steepness of the shape. Fiddle with the main “y=” statement, and you can draw different shapes altogether.
Inserting the blancmange routine into a function and calling it with increasing va (viewing angle), and blanking the screen appropriately, produces a nice animation. It is only practical on a quick machine though, and will be far too slow on a Raspberry Pi.
I originally got the “blankmange” algorithm from a 1980s UK computer magazine. But I can’t recall which one it was, let alone the name of the author. Thanks anyway!