Python programs to paint hyperbolic tilings. They require Python Imaging Library and NumPy (numeric library).

Use a command of the form "python filename.py 2 3 7" — if you give fewer than three numbers, ∞ (infinity) will be used.

Checkerboard designs edit

To show the fundamental regions.

import sys
from PIL import Image
from numpy import array,dot
from numpy.linalg import solve
from math import cos,sin,sqrt,pi
from fractions import Fraction

from numpy import seterr
seterr(all='raise')

# utility functions

width = 2520
xlist = [ float(2*u+1-width)/width for u in xrange(width) ]

def refl(vector,mir): return vector - 2*dot(vector,mir)*mir

pqr = [ eval(s) for s in sys.argv[1:] ]
#pqr.sort()
if len(pqr)>3:
	print "too many numbers; dropping last"
	pqr = pqr[:3]
if sum([ Fraction(1,x) for x in pqr ]) >= 1:
	print "the reciprocals of the numbers must add to less than 1"
	while sum([ Fraction(1,x) for x in pqr ]) >= 1:
		pqr.pop()

filestem = ""
for n in pqr: filestem += repr(n)
while len(filestem) < 3:
	filestem += "i"


# make a list of mirror planes

if not pqr:	# all pairs are asymptotic
	ir3 = 1/sqrt(3)
	mirror = [ array([1j*ir3, 2*ir3, 0]),
		 array([1j*ir3, -ir3, 1]),
		 array([1j*ir3, -ir3, -1])]
else:
	p = pqr.pop(0)
	pangle = pi/p
	cosqr = [ -cos(pi/u) for u in pqr ]
	while len(cosqr) < 2: cosqr.append(-1)

	v0 = [0,1,0]
	v11 = -cos(pangle)
	v12 = sin(pangle)
	v1 = [ 0, v11, v12 ]
	v21 = cosqr[0]
	v22 = (cosqr[1] - v11*v21) / v12
	v2 = [ 1j*sqrt(abs(1-v21**2-v22**2)), v21, v22 ]
	mirror = [ array(v0), array(v1), array(v2) ]

	# Move everything so that the origin is equidistant from the mirrors.

	omnipoint = solve(array(mirror), array([-1,-1,-1]))
	omnipoint /= sqrt(abs(dot(omnipoint,omnipoint)))
	if omnipoint[0].imag < 0: omnipoint = -omnipoint

	tempmirror = omnipoint - array([1j,0,0])
	tempmirror /= sqrt(abs(dot(tempmirror,tempmirror)))

	for j,u in enumerate(mirror):
		v = refl(u,tempmirror)
		if v[0].imag <0: v = -v
		mirror[j] = v

def thecolor( x0,x1 ):
	r2 = x0**2 + x1**2
	if r2 >= 1: return (255,0)
	bottom = 1-r2
	p = array([ 1j*(1+r2)/bottom, 2*x0/bottom, 2*x1/bottom ])

	flip = 0
	clean = 0
	while True:
		for j,u in enumerate(mirror):
			if dot(p,u) > 0:
				p = refl(p,u)
				flip += 1
				clean = 0
			else:
				clean += 1
				if clean >= 3:
					return (255*(flip&1), 255)

im = Image.new("LA", (width,width) )
im.putdata( [ thecolor(x,y)
		for y in xlist
		for x in xlist
		] )
im.save("H2checkers_%s.png" % filestem)

Uniform tilings edit

Creates seven images for seven uniform tilings from a given fundamental triangle. (I haven't yet written the code for the snub tiling.)

If one of your defining numbers is 2, you'll probably want to replace four of these images, using code from the next section.

import sys
from PIL import Image
from numpy import array,dot,cross
from numpy.linalg import solve
from math import cos,sin,sqrt,pi
from fractions import Fraction

from numpy import seterr
seterr(all='raise')

# utility functions

width = 2520
xlist = [ float(2*u+1-width)/width for u in xrange(width) ]

def refl(vector,mir): return vector - 2*dot(vector,mir)*mir

def unit(vector):
	magnitude = sqrt(abs(dot(vector,vector)))
	return vector/magnitude

pqr = [ eval(s) for s in sys.argv[1:] ]
if len(pqr)>3:
	print "too many numbers; dropping last"
	pqr = pqr[:3]
if sum([ Fraction(1,x) for x in pqr ]) >= 1:
	print "the reciprocals of the numbers must add to less than 1"
	while sum([ Fraction(1,x) for x in pqr ]) >= 1:
		pqr.pop()

filestem = ""
for n in pqr: filestem += repr(n)
while len(filestem) < 3:
	filestem += "i"

# make a list of mirror planes

if not pqr:	# all pairs are asymptotic
	ir3 = 1/sqrt(3)
	mirror = [ array([1j*ir3, 2*ir3, 0]),
		 array([1j*ir3, -ir3, -1]),
		 array([1j*ir3, -ir3, 1])]
else:
	p = pqr.pop(0)
	pangle = pi/p
	cosqr = [ -cos(pi/u) for u in pqr ]
	while len(cosqr) < 2: cosqr.append(-1)

	v0 = [0,1,0]
	v11 = -cos(pangle)
	v12 = sin(pangle)
	v1 = [ 0, v11, v12 ]
	v21 = cosqr[0]
	v22 = (cosqr[1] - v11*v21) / v12
	v2 = [ 1j*sqrt(abs(1-v21**2-v22**2)), v21, v22 ]
	mirror = [ array(v0), array(v1), array(v2) ]

	# Move everything so that the origin is equidistant from the mirrors.
	omnipoint = unit(solve(array(mirror), array([-1,-1,-1])))
	if omnipoint[0].imag < 0: omnipoint = -omnipoint
	tempmirror = unit(omnipoint - array([1j,0,0]))
	w = refl(array([1j,0,0]),tempmirror)
	for j,u in enumerate(mirror):
		v = refl(u,tempmirror)
		if v[0].imag <0: v = -v
		mirror[j] = v

for u in mirror: print "mirror", u

# The meat!

def decorate(abc):
	a,b,c = abc
	if a>0 and b<0:
		return (0,0,255,255)
	if c>0:
		return (255,255,0,255)
	return (255,0,0,255)

def thecolor( x0,x1 ):
	r2 = x0**2 + x1**2
	if r2 >= 1: return (0,0,0,0)
	bottom = 1-r2
	p = array([ 1j*(1+r2)/bottom, 2*x0/bottom, 2*x1/bottom ])
	clean = 0
	while True:
		for j,u in enumerate(mirror):
			if dot(p,u) > 0:
				p = refl(p,u)
				clean = 0
			else:
				clean += 1
				if clean >= 3:
					return decorate([ dot(p,u) for u in critplane])

for threebits in range(1,8):
	vertex = solve(array(mirror), array([ -((threebits>>2)&1), -(threebits&1), -((threebits>>1)&1) ]))
	critplane = [ unit(1j*cross(vertex,u)) for u in mirror ]

	im = Image.new("RGBA", (width,width) )
	im.putdata( [ thecolor(x,y)
			for y in xlist
			for x in xlist
			] )
	im.save("H2_tiling_%s-%o.png" % (filestem, threebits))

Uniform tilings with edges edit

Used where two faces of the same color share an edge.

Note that this is NOT a working program: you'll need to make four copies of this file and, in each of them, un-comment the lines appropriate to case 1 (regular), case 3 (truncated), case 4 (regular dual) or case 6 (truncated dual).

BUG: For this code to work right, the first input number must be 2.

import sys
import os
from PIL import Image
from numpy import array,dot,cross
from numpy.linalg import solve
from math import cos,sin,sqrt,pi
from fractions import Fraction

from numpy import seterr
seterr(all='raise')

# utility functions

width = 512
xlist = [ float(2*u+1-width)/width for u in xrange(width) ]

def refl(vector,mir): return vector - 2*dot(vector,mir)*mir

def unit(vector):
	magnitude = sqrt(abs(dot(vector,vector)))
	return vector/magnitude

pqr = [ eval(s) for s in sys.argv[1:] ]
if len(pqr)>3:
	print "too many numbers; dropping last"
	pqr = pqr[:3]
if sum([ Fraction(1,x) for x in pqr ]) >= 1:
	print "the reciprocals of the numbers must add to less than 1"
	while sum([ Fraction(1,x) for x in pqr ]) >= 1:
		pqr.pop()

filestem = ""
for n in pqr: filestem += repr(n)
while len(filestem) < 3:
	filestem += "i"

# make a list of mirror planes

if not pqr:	# all pairs are asymptotic
	ir3 = 1/sqrt(3)
	mirror = [ array([1j*ir3, 2*ir3, 0]),
		 array([1j*ir3, -ir3, -1]),
		 array([1j*ir3, -ir3, 1])]
else:
	p = pqr.pop(0)
	pangle = pi/p
	cosqr = [ -cos(pi/u) for u in pqr ]
	while len(cosqr) < 2: cosqr.append(-1)

	v0 = [0,1,0]
	v11 = -cos(pangle)
	v12 = sin(pangle)
	v1 = [ 0, v11, v12 ]
	v21 = cosqr[0]
	v22 = (cosqr[1] - v11*v21) / v12
	v2 = [ 1j*sqrt(abs(1-v21**2-v22**2)), v21, v22 ]
	mirror = [ array(v0), array(v1), array(v2) ]

	# Move everything so that the origin is equidistant from the mirror.

	omnipoint = unit(solve(array(mirror), array([-1,-1,-1])))
	if omnipoint[0].imag < 0: omnipoint = -omnipoint
	tempmirror = unit(omnipoint - array([1j,0,0]))
	for j,u in enumerate(mirror):
		v = refl(u,tempmirror)
		if v[0].imag <0: v = -v
		mirror[j] = v

for u in mirror: print "mirror", u
v0,v1,v2 = mirror

# The meat!

def thecolor( x0,x1 ):
	r2 = x0**2 + x1**2
	if r2 >= 1: return (255,255,255,0)

	bottom = 1-r2
	p = array([ 1j*(1+r2)/bottom, 2*x0/bottom, 2*x1/bottom ])

	clean = 0
	while True:
		for j,u in enumerate(mirror):
			if dot(p,u) > 0:
				p = refl(p,u)
				clean = 0
			else:
				clean += 1
				if clean >= 3:
					#1:
					#if abs(dot(p,v0)) < 0.02: return (0,0,255,255)
					#return (255,0,0,255)

					#3:
					#if dot(p,critplane) < 0: return (255,0,0,255)
					#if abs(dot(p,v0)) < 0.01: return (0,0,255,255)
					#return (255,255,0,255)

					#4:
					#if abs(dot(p,v1)) < 0.02: return (0,0,255,255)
					#return (255,255,0,255)

					#6:
					#if dot(p,critplane) < 0: return (255,255,0,255)
					#if abs(dot(p,v1)) < 0.01: return (0,0,255,255)
					#return (255,0,0,255)

#3:
#vertex = solve(array(mirror), array([0,1,1]))

#6:
#vertex = solve(array(mirror), array([1,0,1]))

critplane = 1j*cross(vertex,v2)

im = Image.new("RGBA", (width,width) )
im.putdata( [ thecolor(x,y)
		for y in xlist
		for x in xlist
		] )
im.save("%s-%s.png" % (filestem, os.path.splitext(sys.argv[0])[0]))