Chapter 2: Draw Spiro using Turtle Graphics

Chapter 2: Draw Spiro using Turtle Graphics#

Adapted from: “Python Playground: Geeky Projects for the Curious Programmer” by Mahesh Venkitachalam (No Starch Press)

This program demonstrates drawing a spirograph using turtle graphics. This is very similar to using the LOGO language for years gone by.

In file spiro.py#

import sys, random, argparse
import numpy as np
import math
import turtle
import random
from PIL import Image
from datetime import datetime
#from fractions import gcd

# a class that draws a Spirograph
class Spiro:
	# constructor
	def __init__(self, xc, yc, col, R, r, l):

		# create the turtle object
		self.t = turtle.Turtle()
		# set the cursor shape
		self.t.shape('turtle')
		# set the step in degrees
		self.step = 5
		# set the drawing complete flag
		self.drawingComplete = False

		# set the parameters
		self.setparams(xc, yc, col, R, r, l)

		# initialize the drawing
		self.restart()

	# set the parameters	
	def setparams(self, xc, yc, col, R, r, l):
	
		# The spirograph parameters
		self.xc = xc
		self.yc = yc
		self.R = int(R)
		self.r = int(r)
		self.l = l
		self.col = col
		# Reduce r/R to its smallest form by dividing with the GCD
		gcdVal = math.gcd(self.r, self.R)
		self.nRot = self.r // gcdVal
		# get ratio of radii
		self.k = r / float(R)
		# set the color
		self.t.color(*col)
		# store the current angle
		self.a = 0

	# restart the drawing
	def restart(self):
		# set the flag
		self.drawingComplete = False
		# show the turtle
		self.t.showturtle()
		# go to the first point
		self.t.up()
		R, k, l = self.R, self.k, self.l
		a = 0.0
		x = R*((1-k)*math.cos(a) + l*k*math.cos((1-k)*a/k))
		y = R*((1-k)*math.sin(a) + l*k*math.sin((1-k)*a/k))
		self.t.setpos(self.xc + x, self.yc + y)
		self.t.down()


	# draw the whole thing
	def draw(self):
		# draw the rest of the points
		R, k, l = self.R, self.k, self.l
		for i in range(0, 360*self.nRot + 1, self.step):
			a = math.radians(i)
			x = R*((1-k)*math.cos(a) + 1*k*math.cos((1-k)*a/k))
			y = R*((1-k)*math.sin(a) - 1*k*math.sin((1-k)*a/k))
			self.t.setpos(self.xc + x, self.yc + y)
		# drawing is now done so hide the turtle cursor
		self.t.hideturtle()

	# update by one step
	def update(self):
		# skip the rest of the steps if done
		if self.drawingComplete:
			return
		# increment the angle
		self.a += self.step
		# draw a step
		R, k, l = self.R, self.k, self.l
		a = math.radians(self.a)
		x = self.R*((1-k)*math.cos(a) + 1*k*math.cos((1-k)*a/k))
		y = self.R*((1-k)*math.sin(a) - 1*k*math.sin((1-k)*a/k))
		self.t.setpos(self.xc + x, self.yc + y)
		# if drawing is complete, set the flag
		if self.a >= 360*self.nRot:
			self.drawingComplete = True
			# drawing is now done so hide the turtle cursor
			self.t.hideturtle()


# a class for animating Spirographs
class SpiroAnimator:
	# constructor
	def __init__(self, N):
		# set the timer value in milliseconds
		self.deltaT = 10
		# get the window dimensions
		self.width = turtle.window_width()
		self.height = turtle.window_height()
		# create the Spiro objects
		self.spiros = []
		for i in range(N):
			# generate random parameters
			rparams = self.genRandomParams()
			#  set the spiro parameters
			spiro = Spiro(*rparams)
			self.spiros.append(spiro)
		# call timer
		turtle.ontimer(self.update, self.deltaT)

	# generate random parameters
	def genRandomParams(self):
		width, height = self.width, self.height
		R = random.randint(50, min(width, height)//2)
		r = random.randint(10, 9*R//10)
		l = random.uniform(0.1, 0.9)
		xc = random.randint(-width//2, width//2)
		yc = random.randint(-height//2, height//2)
		col = (random.random(), random.random(), random.random())
		return (xc, yc, col, R, r, l)

	# restart spiro drawing
	def restart(self):
		for spiro in self.spiros:
			# clear
			spiro.clear()
			# generate random parameters
			rparams = self.genRandomParams()
			# set the spiro parameters
			spiro.setparams(*rparams)
			# restart the drawing
			spiro.restart()

	def update(self):
		# update all spiros
		nComplete = 0
		for spiro in self.spiros:
			# update
			spiro.update()
			# count completed spiros
			if spiro.drawingComplete:
				nComplete += 1
		# restart if all spiros are complete
		if nComplete == len(self.spiros):
			self.restart()
		# call the timer
		turtle.ontimer(self.update, self.deltaT)

	# toggle turtle cursor on and off
	def toggleTurtles(self):
		for spiro in self.spiros:
			if spiro.t.invisible():
				spiro.t.hideturtle()
			else:
				spiro.t.showturtle()

# save drawings as PNG files
def saveDrawing():
	# hide turtle cursor
	turtle.hideturtle()
	# generate unique filenames
	dateStr = (datetime.now()).strftime("%d%b%Y-%H%M%S")
	fileName = 'spiro-' + dateStr
	print('saving drawing to %s.eps/png' % fileName)
	# get the tkinter canvas
	canvas = turtle.getcanvas()
	# save the drawing as a postscript image
	canvas.postscript(file = fileName + '.eps')
	# use the Pillow module to convert the postscript image file to PNG
	img = Image.open(fileName + '.eps')
	img.save(fileName + '.png', 'png')
	# show the turtle cursor
	turtle.showturtle()

# main() function
def main():
	# use sys.argv if needed
	print('generating spirograph...')
	# create parser
	descStr = """This program draws Spirographs using the Turtle module.
	When run with no arguments, this program draws random Spirographs.

	Terminology:

	R: radius of outer circle
	r: radius of inner circle
	l: ratio of hole distance to r

	"""

	parser = argparse.ArgumentParser(description = descStr)

	# add expected arguments
	parser.add_argument('--sparams', nargs = 3, dest='sparams', required = False, help = "The three arguments in sparams: R, r, l.")

	# parse args
	args = parser.parse_args()

	# set the width of the drawing window to 80 percent of the screen 
	turtle.setup(width = 0.8)

	# set the cursor shape to turtle
	turtle.shape('turtle')

	# set the title to Spirographs!
	turtle.title("Spirographs!")
	# add the key handler to save our drawings
	turtle.onkey(saveDrawing, "s")
	# start listening
	turtle.listen()

	# hide the main turtle cursor
	turtle.hideturtle()

	# check for any arguments sent to --sparams and draw the Spirograph
	if args.sparams:
		params = [float(x) for x in args.sparams]
		# draw the Spirograph with the given parameters
		col = (0.0, 0.0, 0.0)
		spiro = Spiro(0, 0, col, *params)
		spiro.draw()
	else:
		# create the animator object
		spiroAnim = SpiroAnimator(4)
		# add a key handler to toggle the turtle cursor
		turtle.onkey(spiroAnim.toggleTurtles, "t")
		# add a key handler to restart the animation
		turtle.onkey(spiroAnim.restart, "space")
		
	# start the turtle main loop
	turtle.mainloop()

# call main
if __name__ == '__main__':
	main()

The above program is directly into Jupyter Lab and executed by the Python Kernel:

Display the Program Output in Jupyter Lab#

import os
root_dir = ""
root_dir = os.getcwd()
code_dir = root_dir + "/" + "Python_Code/Chapter_2/"
os.chdir(code_dir)

A window opens up with the output. The output is saved to an image file.

from IPython.display import display
from PIL import Image
img_PIL = Image.open(r'Images/spiro.png')
display(img_PIL)
../../../../_images/72dd71db28df90f5be2d6830700a75fcc4b4335d0a06c378363a5c3d80ebd3c2.png