forked from yychiang/Spirograph
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Spirograph.py
122 lines (101 loc) · 4.55 KB
/
Spirograph.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#Author: marktini github.com/marktini
#Spirograph curve drawing program
#Version 2.0
#modified to Python3 by Robin Newman Dec 2018
#additions to allow export of x,y coords using OSC message
#additions to allow remote random colour selection
#with aid of pythonosc library
import math
import turtle
import random
import time
from euclidian import euclidianGCD
from frange import frange
from pythonosc import osc_message_builder
from pythonosc import udp_client
class Spirograph:
#set radius of the spirograph toy (outer circle)
def __init__ (self, R):
self.R = R
self.t = turtle.Turtle() #to be able to access turtle from different methods
#set radius of the inner circle
def setSmallCircle(self, r):
self.r = r
#set distance of pen from inner circle radius; set pen color
def setPen(self, d, color='black', random=False):
self.d = d
self.color = color
self.random = random
#draw the spirograph given current settings
def draw(self,randcol="false"):
#find greatest common denominator of r and R using Euclidian algorithm:
gcd = euclidianGCD(self.r, self.R)
#number of periods is the reduced numerator of the fraction r/R
numPeriods = self.r/gcd
numPetals = self.R/gcd
#calculate constants for graphing
print('Periods: ', numPeriods)
print('Petals: ', numPetals)
k = float(self.r)/self.R
l = float(self.d)/self.r
print('k=',self.r, '/',self.R, '=',k,' l=',self.d,'/',self.r,'=',l)
#use the custom-made frange function to make a list of angles of given increment
angleIncrement = 0.01 #the smaller angleIncrement, the more data points
ptsPeriod = math.ceil(2*math.pi/angleIncrement)
print("Points per Period: ", ptsPeriod)
#frange function is an alternative to range(). The last argument specifies a decimal step
angles = frange(0, 2*math.pi*numPeriods, angleIncrement)
xCoordinates = []
yCoordinates = []
#calculate all the (x,y) points corresponding to parameters in the "angles" list
for theta in angles:
thisX = self.R*((1-k)*math.cos(theta) + l*k*math.cos((1-k)/(k)*(theta)))
thisY = self.R*((1-k)*math.sin(theta) + l*k*math.sin((1-k)/(k)*(theta)))
xCoordinates.append(thisX)
yCoordinates.append(thisY)
print('Num data points: ', len(xCoordinates))
t = self.t #for brevity in future references to the turtle
screen= t.getscreen() #same as above
screen.bgcolor("black")
#name the canvas window
title = "Spirograph with R= " + str(self.R) + ", r = "+str(self.r) + ", and d = " +str(self.d)
screen.title(title)
#for the first point, just move the pen without leaving trace
t.up()
t.goto(xCoordinates[0], yCoordinates[0])
t.down()
#speed up the drawing! update every 20 points. Change these parameters to vary speed
screen.tracer(20)
t.speed(6)
if(randcol=="true"):
randColors = True #if True, change up colors randomly with each period
else:
randColors = False
t.color(self.color)
sender=udp_client.SimpleUDPClient("127.0.0.1",4559)
pointsCount = 0
for each in range(len(xCoordinates)):
t.goto(xCoordinates[each], yCoordinates[each])
pointsCount = pointsCount + 1
#additonal section to export x.y coords using OSC message
if (pointsCount % (numPetals*2) == 0):
sender.send_message('/xcoord',xCoordinates[each])
if (pointsCount % (numPetals*4) ==0):
sender.send_message('/ycoord',yCoordinates[each])
#end of additional section
if (randColors):
if (pointsCount % (ptsPeriod*4) == 0):
red = random.random()
green = random.random()
blue = random.random()
t.color(red, green, blue)
t.hideturtle()
print("Done drawing this curve")
time.sleep(2)
sender.send_message("/finished","done")
#clear the drawing surface after "sec" seconds. Limit time to 2 min just in case
def clear(self, sec):
if sec > 2*60:#reset time in case it's too long
sec = 10
time.sleep(sec)
self.t.getscreen().reset() #now reset the screen