Koch Snowflake

In my collection of programs generating fractals this famous one cannot miss.
The Koch snowflake is generated by starting with an equilateral triangle. Every side of the triangle then gets cut into three equal pieces and the center one gets replaced with yet another equilateral triangle.
To get the perfect fractal, you would need to repeat this process infinitely many times.
More information on the snowflake can be found in this Wikipedia entry.

Controls

  • F1 iterates the fractal
  • F2 zooms in
  • F3 zooms out
  • F4 resets zoom
  • F5 takes a screenshot
  • Arrow keys move the camera around

Koch Snowflake


# Python 2.7.7 Code
# Pygame 1.9.1 (for Python 2.7.7)
# Jonathan Frech 25th of April, 2016
#         edited 26th of April, 2016
#         edited 29th of April, 2016

# importing needed modules
import pygame, sys, time, math, os, random

# define to fill areas
FILL = True
FILLCOLOR = [255, 255, 255]

# define the line's thickness (in pixels)
THICKNESS = 0

# ImageMagick command to create the animated gif
# convert -delay 100 -loop 0 img*.png koch.gif

""" CLASSES """
# line class
class line():
	# init
	def __init__(self, a, b):
		self.a = a
		self.b = b

		self.debug1 = [2, 2]
		self.debug2 = [3, 3]

		self.advanced = False

	# advance the snowflake
	def advance(self):
		# only advance once
		if self.advanced:
			return
		self.advanced = True

		# get initial vector
		vec = vecConvert(self.a, self.b)
		
		# get orthogonal vector
		_vec = [-vec[1], vec[0]]
		
		# calculate orthogonal vector's length and unit vector
		_vecl = vecLen(_vec)
		if _vecl != 0:
			
			# do the same thing for the initial vector
			vecl = vecLen(vec)
			if vecl != 0:
				vec0 = vecMultiply(vec, 1. / vecl)
				vecn = vecMultiply(vec0, vecl / 2.)
				
				# point for orthogonal vector to stick out
				point = vecGetPoint(self.a, vecn)

				# the factor (sqrt(3) / 6) was calculated by using the Pythagorean theorem
				_vec0 = vecMultiply(_vec, 1. / _vecl)
				_vecn = vecMultiply(_vec0, vecl * (math.sqrt(3)/6.))

				# that is the tip of the new triangle
				point2 = vecGetPoint(point, _vecn)

				# draw a line from the tip to the two initial vector's thirds
				third1 = vecGetPoint(self.a, vecMultiply(vec0, vecl / 3.))
				third2 = vecGetPoint(self.a, vecMultiply(vec0, vecl / 3. * 2))
				line1 = line(third1, point2)
				line2 = line(point2, third2)

				# set debug variables
				self.debug1 = point[:]
				self.debug2 = point2[:]

				# calculate filled area
				polygon = [point2, third1, third2]

				# cut the initial vector in three
				l1 = line( vecGetPoint(self.a, vecMultiply(vec0, vecl / 3. * 0)), vecGetPoint(self.a, vecMultiply(vec0, vecl / 3. * 1)) )
				l2 = line( vecGetPoint(self.a, vecMultiply(vec0, vecl / 3. * 1)), vecGetPoint(self.a, vecMultiply(vec0, vecl / 3. * 2)) )
				l3 = line( vecGetPoint(self.a, vecMultiply(vec0, vecl / 3. * 2)), vecGetPoint(self.a, vecMultiply(vec0, vecl / 3. * 3)) )

				# return the new lines and polygon
				return [line1, line2, l1, l3], polygon

	# render line
	def render(self, surface):
		# actual line
		#pygame.draw.line(surface, [255, 255, 255], [(self.a[0] + G.offset[0]) * G.zoom, (self.a[1] + G.offset[1]) * G.zoom], [(self.b[0] + G.offset[0]) * G.zoom, (self.b[1] + G.offset[1]) * G.zoom])
		pygame.draw.line(surface, [255, 255, 255], [self.a[0] * G.zoom + G.offset[0], self.a[1] * G.zoom + G.offset[1]], [self.b[0] * G.zoom + G.offset[0], self.b[1] * G.zoom + G.offset[1]], THICKNESS)

		# debug line
		#pygame.draw.line(surface, [255, 0, 0], self.debug1, self.debug2)

""" FUNCTIONS """
# gets the position on a circle with a position, radius and an angle
def getCirclePos(_pos, _radius, _angle):
	return [
				_pos[0] + _radius * math.cos(math.radians(_angle)),
				_pos[1] + _radius * math.sin(math.radians(_angle))
			]

# basic vector functions
def vecConvert(p1, p2):
	return [p2[0] - p1[0], p2[1] - p1[1]]
def vecLen(vec):
	return math.sqrt( (vec[0]**2) + (vec[1]**2) )
def vecMultiply(vec, n):
	return [vec[0] * n, vec[1] * n]
def vecGetPoint(vec, point):
	return [point[0] + vec[0], point[1] + vec[1]]

""" GAME """
# game class
class GAME():
	# initialize program
	def __init__(self):
		self.width, self.height = 800, 800
		self.size = [self.width, self.height]
		self.surface = pygame.Surface(self.size)
		self.screen = pygame.display.set_mode(self.size)

		self.ticks = 0
		self.running = True

		# calculate initial triangle
		center = [self.width / 2., self.height / 2.]
		r = 400
		A = getCirclePos(center, r, 360. / 3 * 0)
		B = getCirclePos(center, r, 360. / 3 * 1)
		C = getCirclePos(center, r, 360. / 3 * 2)
		self.lines = [line(B, A), line(C, B), line(A, C)]
		
		# fill area?
		if FILL:
			self.polygons = [[A, B, C]]

		# zoom and offset
		self.zoom = 1
		self.zoomdest = 1
		self.offset = [0, 0]
		self.offsetdest = [0, 0]

		# caption
		pygame.display.set_caption("Koch Snowflake")
	
	# reset zoom and offset
	def reset(self):
		self.zoomdest = 1
		self.offsetdest = [0, 0]

	# tick function
	def tick(self):
		# handle events
		for event in pygame.event.get():
			# quit game
			if event.type == pygame.QUIT:
				self.quit()
			
			# handle keys
			if event.type == pygame.KEYDOWN:
				# advance the snowflake
				if event.key == pygame.K_F1:
					# do not overfill the list!
					if len(self.lines) < 1000:
						# generate new triangle's lines
						newlines = []
						for line in self.lines:
							newline, polygon = line.advance()
							
							# new line!
							if newline:
								newlines.append(newline)

							# area?
							if FILL:
								self.polygons.append(polygon)

						# destroy old lines
						self.lines = []

						# add new lines
						for newline in newlines:
							for n in newline:
								self.lines.append(n)

				# zoom in
				elif event.key == pygame.K_F2:
					self.zoomdest *= 2
					self.offsetdest[0] *= 2
					self.offsetdest[1] *= 2

				# zoom out
				elif event.key == pygame.K_F3:
					self.zoomdest /= 2.
					self.offsetdest[0] /= 2
					self.offsetdest[1] /= 2

				# reset zoom and offset
				elif event.key == pygame.K_F4:
					self.reset()

				# save a screenshot
				if event.key == pygame.K_F5:
					self.screenshot()

		# handle held down keys
		keys = pygame.key.get_pressed()
		if keys[pygame.K_UP]:
			self.offsetdest[1] += 10
		elif keys[pygame.K_DOWN]:
			self.offsetdest[1] -= 10
		elif keys[pygame.K_LEFT]:
			self.offsetdest[0] += 10
		elif keys[pygame.K_RIGHT]:
			self.offsetdest[0] -= 10

				

		# approach zoomdest
		self.zoom += (self.zoomdest - self.zoom) / 50.

		# approach offsetdest
		for _ in range(0, 2):
			self.offset[_] += (self.offsetdest[_] - self.offset[_]) / 50.

	# render function
	def render(self):
		# fill
		self.surface.fill([0, 0, 0])

		# render filled area?
		if FILL:
			for polygon in self.polygons:
				p = []
				for _ in polygon:
					p.append([_[0] * G.zoom + G.offset[0], _[1] * G.zoom + G.offset[1]])
				pygame.draw.polygon(self.surface, FILLCOLOR, p, 0)
		
		# render lines
		for line in self.lines:
			line.render(self.surface)

		# blit and flip
		self.screen.blit(self.surface, [0, 0])
		pygame.display.flip()
	
	# quits
	def quit(self):
		self.running = False

	# takes a screenshot
	def screenshot(self):
		path = os.getcwd() + "/out/"
		
		try:
			if not os.path.isdir(path):
				os.mkdir(path)
			
			name = "img" + str(len(os.listdir(path))) + ".png"
			pygame.image.save(self.surface, path + name)
		except:
			pass

	# run function (uses tick() and render())
	def run(self):
		ticksPerSecond = 60
		lastTime = time.time() * 1000000000
		nsPerTick =  1000000000.0 / float(ticksPerSecond)
	
		ticks = 0
		frames = 0
	
		lastTimer = time.time() * 1000
		delta = 0.0
		
		while self.running:
			now = time.time() * 1000000000
			delta += float(now - lastTime) / float(nsPerTick)
			lastTime = now
			shouldRender = False
					
			while delta >= 1:
				ticks += 1
				self.ticks += 1
				self.tick()
				delta -= 1
				shouldRender = True
			
			if shouldRender:
				frames += 1
				self.render()
			
			if time.time() * 1000 - lastTimer >= 1000:
				lastTimer += 1000
				
				# debug
				#print("Frames: " + str(frames) + ", ticks: " + str(ticks))
			
				frames = 0
				ticks = 0

# start game
G = GAME()
G.run()
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s