Generating fancy figures using Python and principles of mathematics.
1. Five-pointed Star
How can we generate a five-pointed star as in the following picture?
First, we import Python's turtle module. This will allow us to draw fancy geometrical shapes. The second line in the code, shape('classic') determines the symbol (the arrow on the left point of the star) that will draw the lines of the star. Speed(15) indicates the speed used to draw the figure. You can use a lower speed, e.g. speed(5) to see how the star is drawn step by step.
Then, we define the function star(), which takes as an input the length of the side of the star and iterates 5 times in a for loop (because we have 5 sides). During each iteration, we use the command forward, to tell our arrow to move forward and the number of units is determined by the length variable. After the arrow finishes drawing one segment, it has to turn in order to draw another star side. At what angle the arrow needs to turn every 5 times in order to draw a five-pointed star? The angle of each corner in such a star is equal to 36°. The arrow will move from the left to the right, drawing a horizontal line (segment 1), then it will turn right to draw a diagonal side of the star (segment 2). The angle of the corner of the star will be 36°, but the angle with which the arrow will turn is 180° - 36° (observe the transition from segment 1 to segment 2). This is done through the command right().
After we are done defining the function, we call it by writing star(200) where 200 is the length of the sides of the star. You can choose any value for this variable.
from turtle import*
shape('classic')
speed(15)
def star(length):
for i in range(5):
forward(length)
right(180-36)
star(200)
2. Harmonograph
A harmonograph is a very old invention, dating back to the 1800s. It basically consisted of a table connected to two pendulums and a pen attached to them. The pendulums swung back and forth and then decayed, while the pen drew interesting patterns on a piece of paper.
In order to draw a harmonograph, we use Python and Processing, a software that helps us create fancy geometrical shapes.
In the code, you can see two equations determining variables x and y which represent the horizontal and vertical displacement of the pen, respectively. The equations below model the oscillation of a pendulum:
x = a1*cos(f1*t + p1)*exp(-d1*t)
y = a2*cos(f2*t + p2)*exp(-d2*t)
Variables a1, a2 are the amplitudes of the motion; f1, f2 are the frequencies; p1, p2 the phase shifts; t the time and d1, d2 the decaying factors.
In the code, time is initiated at the value 0. We define an empty list called points which will be filled later in the code. We will put in this list every point (x,y) run by the oscillator. We will then graph these points in order to make them visible in the output image.
In the setup function, we define the size of the frame. In the draw function, we define the parameters of the oscillation. Line background(255) sets the color of the background to white. After performing a translation of the coordinate system to the center of the screen through translate, and defining the oscillation equations in the variables x and y, we append these values to the points list we created in the beginning. Then we use enumerate in order to go through each element of the points list. In the for loop, i and p refer to the index and the elements of the point list. The if condition is used in order to stop just before the last point (it would give as an error if we tried to connect the last point to the next inexistent point). For this reason, the index i of each point should be strictly smaller than the length of points list - 1. Finally, we draw a line between consecutive points using the line command and increment the time variable by 0.1 units after each iteration.
t=0
points = []
def setup():
size(600,600)
noStroke()
def draw():
global t, points
a1, a2 = 100, 200
f1, f2 = 1,2
p1,p2 = 0,PI/2
d1, d2 = 0.02, 0.02
background(255)
translate(width/2,height/2) #shift center of coordinates in the center
x = a1*cos(f1*t + p1)*exp(-d1*t)
y = a2*cos(f2*t + p2)*exp(-d2*t)
points.append([x,y])
for i,p in enumerate(points):
stroke(0)
if i < len(points) - 1:
line(p[0],p[1],points[i+1][0],points[i+1][1])
t += .1
3. Dynamic Fractal Tree
In order to understand how the dynamic fractal tree is generated, we need to introduce the concept of fractals. What you basically do is define a function and add a call to the function inside the function itself. That sounds a bit confusing so I will try to put it in simpler words: first, do something through a function (e.g. draw a geometrical shape) and then repeat it many times following the same logic but on a smaller scale. This is done through recursion.
Let's have a look at how to build a dynamic fractal tree like the one in our story.
In the setup function, we define the size of the frame. In the draw function, we define the background color as white, translate the center of the coordinate system and through the variable level we define at which depth/level we will create the fractals, i.e. it describes how many times the tree "will open its branches". The Processing's built-in function map() takes as input the x-coordinate of the mouse, which means that the tree will open its branches according to the movement of the mouse in the x-direction. After the code is run the figure generated will be a vertical stick as in the left picture, and as you move the cursor it will be transformed into a beautiful tree with many branches (fractals). The range (0, 25) in the function map determines the range of output or the range of levels we want to draw. If you choose a value lower than 25 the tree will be less dense.
The function y determines the structure of the tree and it takes as input the length of the vertical segment which serves as the trunk of the tree (sz), and the level which determines how many levels of branches (fractals) there will be. The trunk has the highest level value and is created with the function line, whereas the level of the branches decreases by 1, with the last set having the value level = 0. We use the translate function in order to shift the vertical line up the trunk of the tree (in the negative y-direction). Then we use the rotate functions to create the right and left branches.
We use another map() function which takes as an input the y-coordinate of the mouse, called mouseY. This is done to determine the shape of the tree by moving the cursor. In a sense, we are interacting and "waving" at the tree, just like the main character Hoggar was waving at The Friendly Tree.
def setup():
size(600,600)
def draw():
background(255)
translate(300,500)
level = int(map(mouseX,0,width,0,25))
y(100,level)
def y(sz,level):
if level>0:
line(0,0,0,-sz)
translate(0,-sz)
angle = map(mouseY,0,height,0,180)
rotate(radians(angle))
y(0.8*sz,level-1) #right branch
rotate(radians(-2*angle))
y(0.8*sz,level-1) #left branch
rotate(radians(angle))
translate(0,sz)
4. Many circles
Let us draw many circles located in a big circle.
We can draw a circle using the ellipse function, where we define the x- and y- coordinates of the center of the ellipse, and the size in the x- and y-direction. In the draw function we use rotate(radians(t)) to rotate the grid by t degrees, so the circles will be moving around instead of remaining static.
In the for loop, we use rotate(radians(360/12)) to rotate the grid by 360/12 degrees. We use this value in order to place 12 equally spaced circles in a big circle. In the end we increment the time variable by 0.1 units after each iteration.
If you run this code on Processing, you will see 12 circles moving around, just what Hoggar saw when he hurt his head.
t = 0
def setup():
size(600,600)
def draw():
global t
#set background white
background(255)
translate(width/2,height/2)
rotate(radians(t))
for i in range(12):
ellipse(200,100,50,50)
rotate(radians(360/12))
t += 0.1
5. Creating letters using matrices
In our story, the main character managed to create two letters "H" using laces and put them on top of each other. We can generate this result in Python using matrices.
First, we set the range of x- and y-values which determines the size of our window.
The variable transformation_matrix determines the transformation between the two "H" letters. In the setup() function we determine the size of the frame.
In the draw() function we set up the background and grid parameters, the color of lines and the newmatrix variable with determines the transformation between matrices. (more details in the following paragraphs)
def draw():
global xscl,yscl
background(255) #white
translate(width/2,height/2)
grid(xscl, yscl)
ang = map(mouseX,0,width,0,TWO_PI)
rot_matrix = [[cos(ang),-sin(ang)],
[sin(ang),cos(ang)]]
strokeWeight(2) #thicker line
stroke(0) #black
newmatrix = transpose(multmatrix(rot_matrix, transpose(hmatrix)))
graphPoints(hmatrix)
stroke(255,0,0)
graphPoints(newmatrix)
In the hmatrix variable, we determine the coordinates of the points necessary to draw an "H". Further in the code, we define the function graphPoints which takes a matrix as a parameter. We use the Processing's functions beginShape() and endShape() to transform each row into a vertex of the shape. Function graphPoints draws all the points contained in the list hmatrix. Then, we call graphPoints function under the draw function, with hmatrix as input, in order to draw the "H" letter.
def graphPoints(matrix):
#draw line segments between consecutive points
beginShape()
for pt in matrix:
vertex(pt[0]*xscl,pt[1]*yscl)
endShape(CLOSE)
We define rot_matrix in order to rotate the letter "H".
The mulmatrix(a,b) function takes as input two matrices and multiplies them. In order to multiply two matrices, the number of rows in the first matrix should be equal to the number of columns in the second matrix. Under the for i in range(m) cycle we perform the multiplication between the elements of matrix a and matrix b. We append the result to the variable newmatrix.
Transposition is the process of turning the columns of a matrix into rows, and vice versa.
In the transpose function, we create an empty list output where we will append the transposed matrix. The variables m and n define the number of rows and columns of the matrix.
def transpose(a):
'''Transposes matrix a'''
output = []
m = len(a)
n = len(a[0])
#create an n x m matrix
for i in range(n):
output.append([])
for j in range(m):
#replace a[i][j] with a[j][i]
output[i].append(a[j][i])
return output
The hmatrix is transposed before being multiplied by the rotation matrix in the draw() function.
We can make the code more interactive and controlling the rotation of the letter "H" by moving our mouse. In the draw() function we define:
ang = map(mouseX,0,width,0,TWO_PI)
rot_matrix = [[cos(ang),-sin(ang)],
[sin(ang),cos(ang)]]
newmatrix = transpose(multmatrix(rot_matrix, transpose(hmatrix)))
We define the angle of rotation by the x- position of the mouse and plug this variable into the rotation matrix.
Define newmatrix as a multiplication of the rotation matrix and the transpose of the "H" matrix. The color of this new matrix (the second "H") will be red, defined in the stroke(255,0,0) function, where the numbers indicate the RGB colors. Below is the complete code. If you run it on Processing, you will be able to move the new red "H" letter with your mouse.
The complete code:
#set the range of x-values
xmin = -10
xmax = 10
#range of y-values
ymin = -10
ymax = 10
#calculate the range
rangex = xmax - xmin
rangey = ymax - ymin
def setup():
global xscl, yscl
size(600,600)
#the scale factors for drawing on the grid:
xscl = width/rangex
yscl = -height/rangey
noFill()
def draw():
global xscl,yscl
background(255) #white
translate(width/2,height/2)
grid(xscl, yscl)
ang = map(mouseX,0,width,0,TWO_PI)
rot_matrix = [[cos(ang),-sin(ang)],
[sin(ang),cos(ang)]]
strokeWeight(2) #thicker line
stroke(0) #black
newmatrix = transpose(multmatrix(rot_matrix, transpose(hmatrix)))
graphPoints(hmatrix)
stroke(255,0,0)
graphPoints(newmatrix)
hmatrix = [[0,0],[1,0],[1,2],[2,2],[2,0],[3,0],[3,5],[2,5],[2,3],[1,3],[1,5],[0,5]]
def multmatrix(a,b):
#Returns the product of matrix a and matrix b
m = len(a) #number of rows in first matrix
n = len(b[0]) #number of columns in second matrix
newmatrix = []
for i in range(m):
row = []
#for every column in b
for j in range(n):
sum1 = 0
#for every element in the column
for k in range(len(b)):
sum1 += a[i][k]*b[k][j]
row.append(sum1)
newmatrix.append(row)
return newmatrix
def transpose(a):
'''Transposes matrix a'''
output = []
m = len(a)
n = len(a[0])
#create an n x m matrix
for i in range(n):
output.append([])
for j in range(m):
#replace a[i][j] with a[j][i]
output[i].append(a[j][i])
return output
def graphPoints(matrix):
#draw line segments between consecutive points
beginShape()
for pt in matrix:
vertex(pt[0]*xscl,pt[1]*yscl)
endShape(CLOSE)
def grid(xscl,yscl):
'''Draws a grid for graphing'''
#cyan lines
strokeWeight(1)
stroke(0,255,255)
for i in range(xmin, xmax+1):
line(i*xscl,ymin*yscl,i*xscl,ymax*yscl)
for i in range(ymin, ymax+1):
line(xmin*xscl,i*yscl,xmax*xscl,i*yscl)
stroke(0) #black axes
line(0,ymin*yscl,0,ymax*yscl)
line(xmin*xscl,0,xmax*xscl,0)
6. Julia set
We saw a very beautiful image where each point had a pretty color. To determine the color of each point, take a complex number, z (a number made of a real and an imaginary part). Square this number (via cMult) and then keep adding (via cAdd) a constant complex number, c, which has the same value for all points. We do this repeatedly. If the number keeps getting larger, we'll color the pixel corresponding to the original complex number according to how many iterations it takes for its magnitude to get bigger than a certain number, like 2. If the number keeps getting smaller, we'll give it a different color. The magnitude is calculated via the magnitude() function.
This is similar to the case when we multiply a number by another number larger than 1. The result will be larger than the original number. A number multiplied by 1 stays the same, whereas a number multiplied by another number smaller than 1 becomes smaller. A similar thing happens when we use complex numbers. The output will look more complex though because in our formula we also add a constant number, which generates the interesting image we call Julia set. Repeating the multiplication and addition operations on a pixel's location returns a number and if the number never diverges (never becomes larger than 2), we color the pixel black. This is done in the draw() function. To go over all the pixels we use the nested loop for x and y. The julia() function squares and adds the complex number 100 times and returns the number of iterations it took for the number to diverge. The output is assigned to the variable col which determines the color of each pixel. In order to add the colors, in the setup() function we define colorMode(HSB) to let the program know we are using the HSB scale (Hue, Saturation, Brightness), and not the RGB scale (Red, Green, Blue). We can play with the colors using the fill(3*col, 255, 255) line which changes the Hue value according to the col variable.
from math import sqrt
def cAdd(a,b):
'''adds two complex numbers'''
return [a[0]+b[0],a[1]+b[1]]
def cMult(u,v):
'''Returns the product of two complex numbers'''
return [u[0]*v[0]-u[1]*v[1],u[1]*v[0]+u[0]*v[1]]
def magnitude(z):
return sqrt(z[0]**2 + z[1]**2)
def julia(z,c,num):
'''runs the process num times
and returns the diverge count'''
count=0
#define z1 as z
z1=z
#iterate num times
while count <= num:
#check for divergence
if magnitude(z1) >2.0:
#return the step it diverged on
return count
#iterate z
z1 = cAdd(cMult(z1,z1),c)
count += 1
#if z hasn't diverged by the end
return num
#range of x-values
xmin = -2
xmax = 2
#range of y-values
ymin =-2
ymax = 2
#calculate the range
rangex = xmax - xmin
rangey = ymax - ymin
def setup():
global xscl, yscl
size(600,600)
colorMode(HSB)
noStroke()
xscl = float(rangex)/width
yscl = float(rangey)/height
def draw():
#go over all x's and y's on the grid
for x in range(width):
for y in range(height):
z=[(xmin+x*xscl),(ymin+y*yscl)]
c=[0.285,0.01]
#put it into the mandelbrot function
col=julia(z,c,100)
#if mandelbrot returns 0
if col ==100:
fill(0) #make the rectangle black
else:
fill(3*col,255,255)
#draw a tiny rectangle
rect(x,y,1,1)
7. Snowflakes
How do we create snowflakes?
The principle behind the so-called Koch snowflakes is the same as what we used to create the dynamic tree: fractals. The design starts with a simple segment. Then a bump in the shape of a triangle is formed in the middle of this segment. This process happens iteratively.
In the draw() function we call the function snowflake() which takes two parameters: sz (the size of the initial triangle) and level (the level of the fractal).
The snowflake() function draws a triangle with a loop that repeats the code 3 times. Inside this loop we call the segment() function. Let's focus on the segment() function: level 0 means that we will draw just a simple straight line. As the level goes up, we introduce a bump in the middle of the segment, in the shape of a triangle. To create this bump we use the rotate() functions, defining the angles needed in order to draw the sides of the new triangles. We use sz/3 to indicate that the new bump will be created at the length 1/3 of the current segment, and then we introduce the rotate() function to create it. Before rotating, we need to translate to the end of the part of segment where rotation happens (where the bump is created). We do this through the translate() function.
The recursive step is done through the help of the segment() function and going one level down, level-1. Going one level down each iteration avoids entering an infinite loop. We can change the level of fractals from 0 to 7 in an interactive way, by defining the value of level through the map() function. The map() function takes the x-position as a variable and returns an output in the range (0,7). If you move the cursor from left to right, the level of the fractals will increase, transforming our snowflake from a triangle (level 0) to a fancy shape with many bumps (level 7).
def setup():
size(600,600)
def draw():
background(255)
translate(100,200)
level = int(map(mouseX,0,width,0,7))
snowflake(400,level)
def snowflake(sz,level):
for i in range(3):
segment(sz,level)
rotate(radians(120))
def segment(sz,level):
if level == 0:
line(0,0,sz,0)
translate(sz,0)
else:
segment(sz/3.0,level-1)
rotate(radians(-60))
segment(sz/3.0,level-1)
rotate(radians(120))
segment(sz/3.0,level-1)
rotate(radians(-60))
segment(sz/3.0,level-1)
8. Star Spiral
First, we import everything from the turtle module in order to be able to create the star and the spiral. We can use a classic arrow to create our geometrical shape. With the speed() command, we control how fast the spiral is created.
We define the spiral form in the starspiral() function. The length variable determines the length of the first star. Then, we create a for loop which iterates 80 times. This means that the star will rotate 80 times. During each iteration the length of its sides will increase by 3 units. The right() function indicates that the figure will turn to the right and takes as a variable the value of the angle of rotation. Thus, with right(4) we tell the program to rotate the star by 4 degrees at each iteration. The for in range(5) loop creates the star, with 5 sides and inner angles of 36°. The forward(length) function creates one side of the star and the right(180-36) function rotates the arrow in order to create the next side, just as we did in the first paragraph when we created a single five-pointed star.
from turtle import*
shape('classic')
speed(15)
def starspiral():
length=10
for j in range(80):
length+=3
right(4)
for i in range(5):
forward(length)
right(180-36)
starspiral()
Reference: Math adventures with Python (Peter Farrell)
Opmerkingen