Created
October 26, 2017 15:19
-
-
Save PM2Ring/150ed2e2896f129bd49b0714d08e978f to your computer and use it in GitHub Desktop.
Apollonian gasket in GTK2+
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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