Skip to content

Instantly share code, notes, and snippets.

@PM2Ring
Created October 26, 2017 15:19
Show Gist options
  • Save PM2Ring/150ed2e2896f129bd49b0714d08e978f to your computer and use it in GitHub Desktop.
Save PM2Ring/150ed2e2896f129bd49b0714d08e978f to your computer and use it in GitHub Desktop.
Apollonian gasket in GTK2+
#! /usr/bin/env python
''' Draw an Apollonian gasket. Non symmetrical version.
Recursively solves Descartes Theorem to find the circles tangent
to 3 circles that are tangent to each other.
Written by PM 2Ring 2008.09.14
'''
import sys, colorsys
import pygtk; pygtk.require('2.0')
import gtk
#Default image base name
BASENAME = 'Apollo/Apollo%03d.png'
#DrawingArea / Pixmap / Image dimensions
MX, MY = 728, 728
#Colour space names supported by the colorsys module
CSPACES = ('RGB', 'HSV', 'HLS', 'YIQ')
#Put the colorsys RGB to & from conversion functions
#into a dict of tuples keyed by name
def init_colorspace():
fmt0, fmt1 = 'rgb_to_%s', '%s_to_rgb'
f = lambda fmt, name: getattr(colorsys, fmt % name.lower())
return dict((s, (f(fmt0, s), f(fmt1, s))) for s in CSPACES[1:])
colorfunc = init_colorspace()
#Plain RGB interpolation
def interp_rgb(c0, c1, c2):
return [int(.5+sum([c[i] for c in (c0, c1, c2)])/3.) for i in range(3)]
#Colour space interpolation. Needs a better way to handle hue.
def interp_abc(c0, c1, c2):
MC = 65535.
#Convert integer RGB to floats in range [0..1]
rgb = [[c[i]/MC for i in range(3)] for c in (c0, c1, c2)]
#Convert float RGB to ABC
abc = [rgb_to_abc(*c) for c in rgb]
#Interpolate
h3 = [sum([c[i] for c in abc])/3. for i in range(3)]
#Convert to float RGB
c3 = abc_to_rgb(*h3)
#Convert to integer RGB
return [int(.5 + MC*c3[i]) for i in range(3)]
#Colour conversion functions.
interpolate = interp_rgb
#These get set by Circles.combo_cb()
rgb_to_abc, abc_to_rgb = None, None
#----------------------------------------------------------------------
#Circle calculation functions
def ap(a, b, c, s=1):
''' Find a circles tangent to 3 circles that are tangent to each other.
Pass circle curvatures to get the curvature of the tangent circle.
Pass curvatures*centres as complex numbers to get curvatures*centre
of the tangent circle. s selects circle size. Small: 1, Large: -1
'''
#Need to add 0j to handle tiny negative discriminants, which should be 0
return a + b + c + 2. * s *((0j + a*b + b*c + c*a)**.5)
#Find circle tangent to 3 others which are also mutually tangent
def apollo(c0, c1, c2, rs=1, cs=1):
#Find radius
ck = [1./c[2] for c in (c0, c1, c2)] + [rs]
#Need abs to handle tiny negative discriminants, which should be 0
rad = 1. / abs(ap(*ck))
#Position
cz = [(c[0] + 1j*c[1]) / float(c[2]) for c in (c0, c1, c2)] + [cs]
z = rad * ap(*cz)
return [z.real, z.imag, rad]
#distance between 2 circles
def dist(c0, c1):
return abs((c1[0]-c0[0])**2 + (c1[1]-c0[1])**2 - (c1[2]+c0[2])**2)
#product of distances between old circles & new circle
def distcheck(c0, c1, c2, c3):
return dist(c0, c3) * dist(c1, c3) * dist(c2, c3) > .01
#distance between 2 centres
def cdist(c0, c1):
return (c1[0]-c0[0])**2 + (c1[1]-c0[1])**2
#Brute force search for the circle that touches (c0, c1, c4) and is not c2
def bigA(c0, c1, c4, c2):
c = apollo(c0, c1, c4, -1, 1)
if distcheck(c0, c1, c4, c):
c = apollo(c0, c1, c4, -1, -1)
if cdist(c2, c)<.01:
c = apollo(c0, c1, c4, 1, 1)
if distcheck(c0, c1, c4, c):
c = apollo(c0, c1, c4, 1, -1)
if cdist(c2, c)<.01:
print 'Warning: Bad circle location.'
return c
#----------------------------------------------------------------------
class Circles:
radius1 = .425
radius2 = .75
depth = 12
filled = True
pixbuf = None
#Build GUI
def __init__(self):
win = self.win = gtk.Window()
win.connect('destroy', lambda w: gtk.main_quit())
win.set_border_width(5)
hbox = gtk.HBox(spacing=3)
win.add(hbox)
da = self.da = gtk.DrawingArea()
da.set_size_request(MX, MY)
da.connect('expose_event', self.expose_event)
hbox.pack_start(da)
vbox = gtk.VBox()
hbox.pack_start(vbox, False, False, 3)
label = gtk.Label('Depth')
vbox.pack_start(label, False)
adj = gtk.Adjustment(self.depth, 1, 50, 1., 5., 0)
spinner = gtk.SpinButton(adj, 0, 0)
adj.connect('value_changed', self.spin_change, spinner, 'depth')
vbox.pack_start(spinner, False)
self.init_colors()
def add_button(label, action):
button = gtk.Button(label)
vbox.pack_start(button, False)
button.connect('clicked', action)
def add_colorbutton(slabel, name):
label = gtk.Label(slabel)
vbox.pack_start(label, False)
color = getattr(self, name)
colorbutton = gtk.ColorButton(gtk.gdk.Color(*color))
colorbutton.set_title('Select ' + slabel)
colorbutton.connect('color-set', self.color_set_cb, name)
vbox.pack_start(colorbutton, False)
#Create a Frame & VBox to pack next widgets into.
def add_frame():
frame = gtk.Frame()
vbox.pack_start(frame, False, False, 3)
t, v = vbox, gtk.VBox()
frame.add(v)
return t, v
temp, vbox = add_frame()
add_colorbutton('Upper', 'color1')
adj = gtk.Adjustment(self.radius1, 0, 0.999, 0.001, 0.1, 0)
spinner = gtk.SpinButton(adj, 0, 6)
adj.connect('value_changed', self.spin_change, spinner, 'radius1')
vbox.pack_start(spinner, False)
vbox = temp #end frame
temp, vbox = add_frame()
add_colorbutton('Lower', 'color2')
adj = gtk.Adjustment(self.radius2, 0, 1, 0.001, 0.1, 0)
spinner = gtk.SpinButton(adj, 0, 6)
adj.connect('value_changed', self.spin_change, spinner, 'radius2')
vbox.pack_start(spinner, False)
vbox = temp #end frame
add_colorbutton('Inner', 'color4')
add_colorbutton('Outer', 'color0')
add_colorbutton('Background', 'color3')
separator = gtk.HSeparator()
vbox.pack_start(separator, False, False, 3)
label = gtk.Label('Colour Space')
vbox.pack_start(label, False)
combobox = gtk.combo_box_new_text()
for i in CSPACES:
combobox.append_text(i)
vbox.pack_start(combobox, False)
combobox.set_active(0)
combobox.connect('changed', self.combo_cb)
button = gtk.CheckButton('_Filled')
button.set_active(self.filled)
vbox.pack_start(button, False)
button.connect('clicked', self.filled_button_callback)
separator = gtk.HSeparator()
vbox.pack_start(separator, False, False, 3)
add_button('_Draw', lambda widget, win=win: self.do_gasket())
add_button('_Clear', lambda widget, win=win: self.clear_pixmap())
add_button('_Save', lambda widget, win=win: self.save_pixmap())
add_button('_Quit', lambda widget, win=win: win.destroy())
win.show_all()
self.init_pixmap(MX, MY)
self.do_gasket()
def expose_event(self, widget, event):
x, y, width, height = event.area
widget.window.draw_drawable(self.fg, self.pixmap, x, y, x, y, width, height)
return False
#Toggle filled or unfilled circles
def filled_button_callback(self, widget):
self.filled = widget.get_active()
#Select interpolation & colourspace conversion functions
def combo_cb(self, combobox):
global interpolate, rgb_to_abc, abc_to_rgb
model = combobox.get_model()
k = combobox.get_active()
if k < 0:
return
abc = model[k][0]
if k:
interpolate = interp_abc
rgb_to_abc, abc_to_rgb = colorfunc[abc]
else:
interpolate = interp_rgb
# Spin button callback. Update self attribute 'name' with current value
def spin_change(self, widget, spinner, name):
value = spinner.get_value()
setattr(self, name, value)
def color_set_cb(self, colorbutton, name):
color = colorbutton.get_color()
value = [color.red, color.green, color.blue]
setattr(self, name, value)
#Initial colour palette
def init_colors(self):
#Blue, Purple, Green on Yellow
self.color0 = [59881, 57386, 47905]
self.color1 = [24672, 32125, 49087]
self.color2 = [43433, 0, 41377]
self.color3 = [0, 0, 0]
self.color4 = [0, 39321, 0]
def print_colors(self):
for n in ['color%d' % i for i in range(5)]:
print 'self.%s = %s' % (n, getattr(self, n))
def init_pixmap(self, width, height):
self.pixmap = gtk.gdk.Pixmap(self.da.window, width, height)
self.gc = self.pixmap.new_gc()
self.cm = self.da.get_colormap()
self.fg = self.da.get_style().fg_gc[gtk.STATE_NORMAL]
self.cx, self.cy = width // 2, height // 2
self.clear_pixmap()
def clear_pixmap(self):
width, height = self.pixmap.get_size()
self.gc.foreground = self.cm.alloc_color(*self.color3)
self.pixmap.draw_rectangle(self.gc, True, 0, 0, width, height)
self.da.queue_draw()
def put_circle(self, x, y, rad, r, g, b):
rad = int(.5+rad)
x, y = int(.5+x) - rad, int(.5+y) - rad
d = 2 * rad
self.gc.foreground = self.cm.alloc_color(r, g, b)
self.pixmap.draw_arc(self.gc, self.filled, x, y, d, d, 0, 360*64)
self.da.queue_draw_area(x, y, d+1, d+1)
#----------------------------------------------------------------------
#Find touching circle & plot it.
def soddy(self, c0, c1, c2):
c = apollo(c0, c1, c2) + interpolate(c0[3:], c1[3:], c2[3:])
self.put_circle(*c)
return c
def soddyA(self, c0, c1, c4, c2):
c = bigA(c0, c1, c4, c2) + interpolate(c0[3:], c1[3:], c4[3:])
self.put_circle(*c)
return c
#Recursive Apollonian gasket
def gasket(self, c0, c1, c2, depth):
depth -= 1
if depth>0:
c3 = self.soddy(c0, c1, c2)
if c3[2] > 2:
self.gasket(c0, c1, c3, depth)
self.gasket(c0, c3, c2, depth)
self.gasket(c3, c1, c2, depth)
def gasketA(self, c0, c1, c4, c2, depth):
depth -= 1
if depth>0:
c5 = self.soddyA(c0, c1, c4, c2)
if c5[2] > 2:
self.gasketA(c0, c1, c5, c4, depth)
self.gasketA(c0, c4, c5, c1, depth)
self.gasket(c1, c4, c5, depth)
def do_gasket(self):
cx, cy = self.cx, self.cy
r0 = int(cy * .975)
r1 = r0 * self.radius1
r2 = (r0 - r1) * self.radius2
u = (r0*r1 + r0*r2 + r1*r2 - r0*r0) / (r0 - r1)
v = abs((r0-r2)**2 - u**2)**.5
c0 = [cx, cy, r0] + self.color0
c1 = [cx, cy-r0+r1, r1] + self.color1
c2 = [cx+v, cy+u, r2] + self.color2
for c in (c0, c1, c2):
self.put_circle(*c)
#Negate curvature for outer circle
c0[2] *= -1
#Recursively fill behind c1 & c2
self.gasket(c0, c1, c2, int(self.depth))
#gtk.main_iteration()
#Get the larger circle that touches c1 & c2
c4 = apollo(c0, c1, c2, -1, -1) + self.color4
self.put_circle(*c4)
#Fill remaining spots
self.gasketA(c0, c1, c4, c2, int(self.depth))
self.gasketA(c0, c2, c4, c1, int(self.depth))
self.gasket(c1, c2, c4, int(self.depth-1))
def save_pixmap(self):
global savecount
width, height = self.pixmap.get_size()
if self.pixbuf == None:
self.pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, width, height)
self.pixbuf.get_from_drawable(self.pixmap, self.cm, 0,0, 0,0, width, height)
fname = basename % savecount
savecount += 1
self.pixbuf.save(fname, filetype)
print '%s saved' % fname
self.print_colors()
if __name__ == '__main__':
basename = len(sys.argv) <= 1 and BASENAME or sys.argv[1]
savecount = len(sys.argv) > 2 and int(sys.argv[2]) or 0
#Determine filetype from the extension.
ext = basename.split('.')[-1].lower()
for d in gtk.gdk.pixbuf_get_formats():
if ext in d['extensions']:
filetype = d['name']
break
else:
sys.exit('Unknown file extension: %s' % ext)
Circles()
gtk.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment