Skip to content

Instantly share code, notes, and snippets.

@luca-drf
Last active June 19, 2017 23:30
Show Gist options
  • Save luca-drf/902884b25931e9dd281835534d56bfe3 to your computer and use it in GitHub Desktop.
Save luca-drf/902884b25931e9dd281835534d56bfe3 to your computer and use it in GitHub Desktop.
Sierpinski Triangle Explorer

Sierpinski Triangle Explorer

Requirements

  • Python 2.7
  • pytest
  • PyGObject
  • Gtk 3

Requirements Installation

Install Gtk3 and PyGObject with:

macOS (via Homebrew):

$ brew install python gtk+3 pygobject3

Ubuntu:

$ sudo apt-get python-gi python-gi-cairo

Pytest can be installed via pip:

$ pip install pytest

To get the code simply git clone this Gist.

Unit Tests

Run the unit tests from within the cloned folder with:

$ pytest

Usage

Run the Sierpinski Triangle Explorer from within the cloned folder with:

$ python sierpinski_explorer.py

You can zoom in/out the fractal scrolling (mouse wheel) over the image

You can pan the fractal by dragging it with the mouse cursor (the fractal will be re-drawn upon button release)

#!/usr/local/bin/python
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
import math
class Sierpinski(Gtk.Window):
def __init__(self):
super(Sierpinski, self).__init__()
self.drag_coords = []
self.coords = self.get_coords([50, 400], 400)
self.increment_p = 0.10
self.tresh = 800
self.depths = 3
self._add_depths = 1
self.init_ui()
@property
def length(self):
"""Outermost triangle's side length"""
return abs(self.coords[2][0] - self.coords[0][0])
def init_ui(self):
self.darea = Gtk.DrawingArea()
self.darea.connect("draw", self.on_draw)
self.darea.set_events(Gdk.EventMask.ALL_EVENTS_MASK)
self.add(self.darea)
self.darea.connect("button-press-event", self.on_button_press)
self.darea.connect("button-release-event", self.on_button_release)
self.darea.connect("scroll-event", self.on_scroll)
self.set_title("Sierpinski Triangle Explorer")
self.resize(500, 500)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", Gtk.main_quit)
self.show_all()
def mid_point(self, a, b):
'''Returns the coordinates of the middle point found on the segment
passing between point a and point b'''
return ((a[0] + b[0]) / 2, (a[1] + b[1]) / 2)
def get_coords(self, bott_left, length):
"""Returns a list containing three pairs of coordinates representing
the three vertices of an equilateral triangle having bott_left
coordinates as bottom left vertex and side's length as length"""
height = length * math.sqrt(3) / 2
top = [(length / 2) + bott_left[0], bott_left[1] - height]
bott_right = [bott_left[0] + length, bott_left[1]]
return [bott_left, top, bott_right]
def move_coords(self):
'''Translates - updating in place - self.coords by the same amount of
space (and maintaining the direction) separating the two points in
self.drag_coords'''
if len(self.drag_coords) == 2:
delta_x = (self.drag_coords[1][0] - self.drag_coords[0][0])
delta_y = (self.drag_coords[1][1] - self.drag_coords[0][1])
for coord in self.coords:
coord[0] += delta_x
coord[1] += delta_y
def zoom_coords(self, p):
'''Increaases (or decreases) the distance beetween the three points in
self.coords maintaining the triangle proportions, by percentage p.
p value must be between -1 and 1 (e.g. -0.1 decreases the distance by 10%)'''
bott_left = self.coords[0]
bott_right = self.coords[2]
increment = p * self.length
new_length = self.length + increment
bott_left[0] -= (increment / 2)
bott_left[1] += (increment / 2)
self.coords = self.get_coords(bott_left, new_length)
def _draw_triangle(self, coords, cr):
'''Draws three straight lines between respectively 0, 1, 2 points in
coords'''
cr.move_to(coords[0][0], coords[0][1])
cr.line_to(coords[1][0], coords[1][1])
cr.line_to(coords[2][0], coords[2][1])
cr.line_to(coords[0][0], coords[0][1])
cr.stroke()
def _draw_fractal(self, coords, depth, cr):
'''Draws Sierpinski fractal, given three points (coords) and a positive
integer number (depths) representing respectively the three vertices of
a triangle and the level of sub-triangles to draw.
Eg. depth == 0 --> draws a single triangle
depth == 1 --> draws a "Triforce"
depth == 2 --> draws a "Triforce" in the perimetral triangles
...
'''
self._draw_triangle(coords, cr)
if depth > 0:
self._draw_fractal([coords[0],
self.mid_point(coords[0], coords[1]),
self.mid_point(coords[0], coords[2])],
depth - 1, cr)
self._draw_fractal([coords[1],
self.mid_point(coords[0], coords[1]),
self.mid_point(coords[1], coords[2])],
depth - 1, cr)
self._draw_fractal([coords[2],
self.mid_point(coords[2], coords[1]),
self.mid_point(coords[0], coords[2])],
depth - 1, cr)
def on_draw(self, wid, cr):
cr.set_source_rgb(0, 0, 0)
cr.set_line_width(1)
self._draw_fractal(self.coords, self.depths + self._add_depths, cr)
def on_button_press(self, w, e):
'''Saves the coordinates of the mouse pointer on the drawing area when
the left button is pressed'''
self.drag_coords.append((e.x, e.y))
def on_button_release(self, w, e):
'''Saves the coordinates of the mouse pointer on the drawing area when
the left button is released. Then calls self.move_coords to translate
the figure coordinates and re-draws the figure.'''
self.drag_coords.append((e.x, e.y))
self.move_coords()
self.drag_coords = []
self.darea.queue_draw()
def on_scroll(self, w, e):
'''Zooms in and out when the mouse wheel (scroll) is rotated'''
if e.delta_y == -1:
self.zoom_coords(self.increment_p)
if int(self.length / self.tresh) > 0:
self._add_depths += 1
self.tresh = self.tresh * 2
self.darea.queue_draw()
elif e.delta_y == 1 and self.length > 16:
self.zoom_coords(-self.increment_p)
if int(self.length / (self.tresh / 2)) == 0:
if self._add_depths > 1:
self._add_depths -= 1
self.tresh = self.tresh / 2
self.darea.queue_draw()
def main():
app = Sierpinski()
Gtk.main()
if __name__ == "__main__":
main()
import pytest
from sierpinski_explorer import Sierpinski
def test_zero_length():
s = Sierpinski()
s.coords = [[50, 0], [0, 0], [50, 0]]
assert s.length == 0
def test_pos_neg_length():
s = Sierpinski()
s.coords = [[50, 0], [0, 0], [-10, 0]]
assert s.length == 60
def test_neg_pos_length():
s = Sierpinski()
s.coords = [[-50, 0], [0, 0], [10, 0]]
assert s.length == 60
def test_neg_neg_length():
s = Sierpinski()
s.coords = [[-50, 0], [0, 0], [-10, 0]]
assert s.length == 40
def test_null_length():
s = Sierpinski()
s.coords = [[0, 0], [0, 0], [0, 0]]
assert s.length == 0
def test_pos_mid_point():
s = Sierpinski()
a = [20, 20]
b = [40, 40]
assert s.mid_point(a, b) == (30, 30)
def test_neg_pos_mid_point():
s = Sierpinski()
a = [-20, -20]
b = [40, 40]
assert s.mid_point(a, b) == (10, 10)
def test_pos_neg_mid_point():
s = Sierpinski()
a = [20, 20]
b = [-40, -40]
assert s.mid_point(a, b) == (-10, -10)
def test_get_coords():
s = Sierpinski()
a = [100, 250]
expected = [a, [200, 76.79491924311228], [300, 250]]
assert s.get_coords(a, 200) == expected
def test_get_coords_neg():
s = Sierpinski()
a = [-100, 250]
expected = [[-100, 250], [0, 76.79491924311228], [100, 250]]
assert s.get_coords(a, 200) == expected
def test_move_coords():
s = Sierpinski()
s.coords = [[10, 10], [20, 0], [30, 10]]
s.drag_coords = [[1, 1], [5, 5]]
s.move_coords()
assert s.coords == [[14, 14], [24, 4], [34, 14]]
def test_zoom_coords():
s = Sierpinski()
s.coords = [[10, 10], [20, 0], [30, 10]]
expected = [[9, 11], [20, -8.05255888325765], [31, 11]]
s.zoom_coords(0.10)
assert s.coords == expected
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment