# Written by Mark Shuttleworth, 2014, for Python 3.4. The program generates clusters of concentric spirographs 
# (flowers) each of which have random major and minor gear wheel radii and teeth counts. The plot colour 
# rotates through yuv space with the plotting of the graph so that when the ends join up, their colours match.

import turtle 				# Only one import, that means we'll have to do...

pi = 3.141592653589793238	#...a definition of pi...

def sin(x):				#...a Taylor/Maclaurin expansion of sin(x)...
		x = x % (2 * pi)				# Series is only good over a limited range
		xs = -(x * x)					# Work out minus x squared in advance rather than in loop
		xterm = x * xs					# Set up numerator and denominator to start with cubed term
		fterm = 6.0
		for i in range (4,18,2):		# Less than x^^19 gives discontinuities (up to x^^17 done in loop)
			x = x + (xterm / fterm)
			xterm = xterm * xs
			fterm = fterm * i * (i + 1)
		return (x + (xterm / fterm))	# Use the x^^19 terms we calculated before we dropped out of the loop 
		
def rand(min, max):		#...and a random number generator (based on c code for xorshift+ from Wikipedia and giving out an int between min and max) the hard way.
	s1 = rand.random & 0xFFFFFFFFFFFFFFFF				# We use rand.random function attribute as a static/global to maintain our random number between calls (not very nice, but compact)
	s0 = (rand.random >> 64)  & 0xFFFFFFFFFFFFFFFF		# the 'and'ing with 16Fs is to emulate 64-bit fixed length registers/variables - python just keeps expanding its long to accomodate whatever you've got otherwise
	s1 = (s1 ^ (s1 << 23))  & 0xFFFFFFFFFFFFFFFF
	s1 = ((s1 ^ s0 ^ (s1 >> 17) ^ (s0 >> 26)) + s0) & 0xFFFFFFFFFFFFFFFF
	rand.random = s0 + (s1 << 64)
	return min + int((max + 1 - min) * (rand.random / int(1 << 128)))

def numturns(maj, min):		# We can find the minimum number of complete turns required by removing common prime factors
	searchlim = min + 1
	while ((maj % 2) == 0):	# between the major and minor teeth numbers. The answer is what's left of the minor
		maj = maj // 2			# teeth number. First of all we eliminate two as a factor, coz then we only have half
		if ((min % 2) == 0):	# as many checks to do, as we can use a step size of two in the for loop
			min = min // 2
	for factor in range (3, (searchlim), 2): # Then we do it for any remaining factors, note factors that are not prime
		while ((maj % factor) == 0):		# eg 6, have already been eliminated as their constituent, smaller prime 
			maj = maj // factor				# factors (eg 2, 3) have already been removed, so the 'while (maj % factor)' test will fail.
			if ((min % factor) == 0):
				min = min // factor
	return min

def colourcalc(y, u, v):		# Function takes a uv vector and random y value to generate rgb values
	r = (y + (v / 0.877))		# These constants are wrong, but I'm out of time and it works roughly so...c'est la vie
	g = (y - (0.395  * u) - (0.581 * v))
	b = (y + (u / 0.492))
	r = 0 if (r < 0) else (1 if (r > 1) else r)	# Limit the rgb values between 0 and 1 
	g = 0 if (g < 0) else (1 if (g > 1) else g)
	b = 0 if (b < 0) else (1 if (b > 1) else b)
	return(r,g,b)

seedstring = input("Enter a word or phrase to act as a random number seed: ")	# Initialise random number generator with a seed derived from a user supplied word
seedlen = len(seedstring)
if seedlen == 0:
	seed = 1													# Make sure we don't end up with '0'
else:
	if seedlen >=16:											# Use the first 16 letters to create a seed value
		seedlen = 16
	seed = 0
	for iter in range (0, seedlen):
		seed = seed * 256 + ord(seedstring[iter])
rand.random = seed												# Set the random number to the seed value
for loop in range (0,10):										# Run a few iterations of the generator to propogate some bits through (otherwise first few values are zeroish)
	rand(0, 0)

myrtle = turtle.Turtle()										# Setup our turtle, called myrtle
myrtle.penup()													# Put its pen up

flowercount = rand (1,20)										# How many spriograph flowers shall we do on our paper?
stepsperloop = 30												# For each smaller wheel loop, how many points should we have to give a smooth curve (30 seems good)

for i in range(0,flowercount):									# For each flower, generate a centrepoint and how many individual spirographs will be centred on it
	centrex = rand(-400,400)
	centrey = rand(-300,300)
	numconc = rand(1, 4)

	for j in range (0,numconc):								# For each individual spirograph, generate a major and minor radius and number of teeth
		majorrad = rand(25,100)
		minorrad = rand(0.1 * majorrad, 2 * majorrad)
		majorteeth = rand(25,50)
		minorteeth = rand(5,majorteeth-1)

		stepsperturn = int(stepsperloop * majorteeth / minorteeth) # Pre-calculate some stepping variables outside the main loop
		turns = numturns(majorteeth, minorteeth)
		majorstep = 2 * pi /stepsperturn
		minorstep = (majorteeth / minorteeth) * majorstep
		uvstep = (2 * pi / (stepsperturn * turns))
		yval = rand(0,255)/255

		myrtle.goto(centrex, centrey + majorrad + minorrad) # Get the pen in place before we put it down
		myrtle.pendown()
		
		for k in range (0, (stepsperturn * turns) + 1):	# Main point drawing loop
			uval = sin(k * uvstep)						# Calculate colour vector and generate rgb values
			vval = sin((k * uvstep) + pi/2)
			myrtle.pencolor(colourcalc(yval,uval,vval))
			x = centrex + majorrad * sin(k * majorstep) + minorrad * sin(k * minorstep) # Calc x,y coords and draw to there
			y = centrey + majorrad * sin(k * majorstep + pi / 2) + minorrad * sin(k * minorstep + pi / 2)
			myrtle.goto(x, y)

		myrtle.penup()	# Finished that graph, pen up before starting the next
input("Press Enter Key To Finish")	# Completely finished, hang around 'til user confirms